04/06 adding some additional functions
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Edit' if item else 'New' }} Quick Reply{% endblock %}
|
||||
{% block page_title %}{{ 'Edit' if item else 'New' }} Quick Reply{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-chat-square-text me-2"></i>
|
||||
{{ 'Edit Quick Reply' if item else 'Create Quick Reply' }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title *
|
||||
<span class="text-muted fw-normal" style="font-size:12px;">
|
||||
— shown in the dropdown menu on ticket comments
|
||||
</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="title" required maxlength="120"
|
||||
value="{{ item.title if item else '' }}"
|
||||
placeholder="e.g. Please restart and confirm"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Category
|
||||
<span class="text-muted fw-normal" style="font-size:12px;">— optional grouping label</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" maxlength="50"
|
||||
value="{{ item.category if item and item.category else '' }}"
|
||||
placeholder="e.g. Hardware, Software, Escalation"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Response Body *
|
||||
<span class="text-muted fw-normal" style="font-size:12px;">— Markdown supported</span>
|
||||
</label>
|
||||
<textarea class="form-control" name="body" rows="8" required
|
||||
placeholder="Please restart your device and let us know if the issue persists.">{{ item.body if item else '' }}</textarea>
|
||||
<div class="form-text">
|
||||
Use **bold**, _italic_, `code`, and - lists. This text will be inserted
|
||||
directly into the comment box when selected.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if item %}
|
||||
<div class="mb-4 d-flex align-items-center gap-2">
|
||||
<input type="checkbox" id="is_active" name="is_active"
|
||||
{% if item.is_active %}checked{% endif %}
|
||||
style="accent-color:var(--accent);width:16px;height:16px;"/>
|
||||
<label for="is_active" style="font-size:14px;margin:0;cursor:pointer;">
|
||||
Active (visible to IT staff in the quick replies dropdown)
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check2 me-2"></i>{{ 'Save Changes' if item else 'Create Quick Reply' }}
|
||||
</button>
|
||||
<a href="{{ url_for('admin.canned_responses') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Quick Replies{% endblock %}
|
||||
{% block page_title %}Quick Replies{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<p style="color:var(--muted);font-size:13px;margin:0;">
|
||||
Pre-written responses that IT staff can insert into ticket comments with one click.
|
||||
</p>
|
||||
<a href="{{ url_for('admin.canned_response_new') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>New Quick Reply
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if items %}
|
||||
{% set ns = namespace(last_cat='') %}
|
||||
{% for item in items %}
|
||||
{% if item.category and item.category != ns.last_cat %}
|
||||
<h6 style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;
|
||||
color:var(--muted);margin:20px 0 8px;">{{ item.category }}</h6>
|
||||
{% set ns.last_cat = item.category %}
|
||||
{% elif not item.category and ns.last_cat %}
|
||||
<h6 style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;
|
||||
color:var(--muted);margin:20px 0 8px;">Uncategorised</h6>
|
||||
{% set ns.last_cat = '' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card mb-2 {% if not item.is_active %}opacity-50{% endif %}">
|
||||
<div class="card-body py-3 px-4 d-flex align-items-start gap-3">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span style="font-size:14px;font-weight:600;">{{ item.title }}</span>
|
||||
{% if not item.is_active %}
|
||||
<span class="badge bg-secondary" style="font-size:10px;">Inactive</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--muted);white-space:pre-wrap;max-height:60px;overflow:hidden;text-overflow:ellipsis;">{{ item.body }}</div>
|
||||
<div style="font-size:11px;color:var(--muted2);margin-top:4px;">
|
||||
Added by {{ item.creator.full_name }} · {{ item.created_at.strftime('%b %d, %Y') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-shrink-0">
|
||||
<a href="{{ url_for('admin.canned_response_edit', item_id=item.id) }}"
|
||||
class="btn btn-secondary btn-sm"><i class="bi bi-pencil"></i></a>
|
||||
<form method="POST"
|
||||
action="{{ url_for('admin.canned_response_delete', item_id=item.id) }}"
|
||||
onsubmit="return confirm('Delete this quick reply?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-chat-square-text" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
No quick replies yet. Create one to speed up IT responses.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -73,11 +73,51 @@
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">New Password</label>
|
||||
<input type="password" class="form-control" name="new_password" placeholder="Min. 8 characters"/>
|
||||
<!-- Show/hide toggle wrapper -->
|
||||
<div style="position:relative;">
|
||||
<input type="password" class="form-control" name="new_password"
|
||||
id="prof-pw" placeholder="Min. 8 characters"
|
||||
autocomplete="new-password"
|
||||
oninput="profPwInput()" style="padding-right:40px;"/>
|
||||
<button type="button" id="prof-pw-toggle"
|
||||
onclick="profToggle('prof-pw','prof-pw-toggle')"
|
||||
tabindex="-1"
|
||||
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);
|
||||
background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Strength bar (hidden until typing starts) -->
|
||||
<div id="prof-strength-wrap" style="display:none;margin-top:6px;">
|
||||
<div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:5px;">
|
||||
<div id="prof-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
|
||||
</div>
|
||||
<div id="prof-strength-label" style="font-size:11px;font-weight:600;font-family:'Space Mono',monospace;display:flex;align-items:center;gap:5px;"></div>
|
||||
</div>
|
||||
<!-- Rule checklist -->
|
||||
<div id="prof-rules" style="display:none;margin-top:7px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 12px;display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;">
|
||||
<div class="prof-rule" id="prof-rule-len" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> 8+ characters</div>
|
||||
<div class="prof-rule" id="prof-rule-upper" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Uppercase letter</div>
|
||||
<div class="prof-rule" id="prof-rule-digit" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Number</div>
|
||||
<div class="prof-rule" id="prof-rule-special"style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Special character</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Confirm Password</label>
|
||||
<input type="password" class="form-control" name="confirm_password" placeholder="Repeat password"/>
|
||||
<div style="position:relative;">
|
||||
<input type="password" class="form-control" name="confirm_password"
|
||||
id="prof-pw2" placeholder="Repeat password"
|
||||
autocomplete="new-password"
|
||||
oninput="profConfirmInput()" style="padding-right:40px;"/>
|
||||
<button type="button" id="prof-pw2-toggle"
|
||||
onclick="profToggle('prof-pw2','prof-pw2-toggle')"
|
||||
tabindex="-1"
|
||||
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);
|
||||
background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="prof-match-msg" style="font-size:11.5px;margin-top:6px;font-family:'Space Mono',monospace;font-weight:600;min-height:18px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,4 +130,84 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── Profile page password strength — mirrors register.html rules exactly ──────
|
||||
const PROF_RULES = {
|
||||
len: pw => pw.length >= 8,
|
||||
upper: pw => /[A-Z]/.test(pw),
|
||||
digit: pw => /\d/.test(pw),
|
||||
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\|`~]/.test(pw),
|
||||
};
|
||||
const PROF_LEVELS = [
|
||||
{ label:'Very Weak', color:'#ef4444', pct:12 },
|
||||
{ label:'Weak', color:'#f97316', pct:30 },
|
||||
{ label:'Fair', color:'#eab308', pct:52 },
|
||||
{ label:'Good', color:'#22c55e', pct:76 },
|
||||
{ label:'Strong', color:'#059669', pct:100 },
|
||||
];
|
||||
|
||||
function profScore(pw) {
|
||||
return [PROF_RULES.len,PROF_RULES.upper,PROF_RULES.digit,PROF_RULES.special]
|
||||
.filter(r => r(pw)).length + (pw.length >= 16 ? 1 : 0);
|
||||
}
|
||||
function profSetRule(id, met) {
|
||||
const el = document.getElementById('prof-rule-' + id);
|
||||
if (!el) return;
|
||||
el.style.color = met ? 'var(--success)' : 'var(--muted)';
|
||||
el.querySelector('.prof-ri').innerHTML = met
|
||||
? '<i class="bi bi-check-circle-fill" style="color:var(--success);"></i>'
|
||||
: '<i class="bi bi-circle"></i>';
|
||||
}
|
||||
function profPwInput() {
|
||||
const pw = document.getElementById('prof-pw').value;
|
||||
const wrap = document.getElementById('prof-strength-wrap');
|
||||
const rules = document.getElementById('prof-rules');
|
||||
const bar = document.getElementById('prof-strength-bar');
|
||||
const lbl = document.getElementById('prof-strength-label');
|
||||
if (!pw) {
|
||||
wrap.style.display = 'none'; rules.style.display = 'none';
|
||||
document.getElementById('prof-pw').classList.remove('is-valid','is-invalid');
|
||||
profConfirmInput(); return;
|
||||
}
|
||||
wrap.style.display = 'block'; rules.style.display = 'grid';
|
||||
profSetRule('len', PROF_RULES.len(pw));
|
||||
profSetRule('upper', PROF_RULES.upper(pw));
|
||||
profSetRule('digit', PROF_RULES.digit(pw));
|
||||
profSetRule('special', PROF_RULES.special(pw));
|
||||
const score = profScore(pw);
|
||||
const lvl = PROF_LEVELS[Math.max(0, score - 1)] || PROF_LEVELS[0];
|
||||
bar.style.width = lvl.pct + '%';
|
||||
bar.style.background = lvl.color;
|
||||
lbl.style.color = lvl.color;
|
||||
const icon = score >= 4 ? 'shield-fill' : score >= 2 ? 'shield-half' : 'shield';
|
||||
lbl.innerHTML = `<i class="bi bi-${icon}"></i> ${lvl.label}`;
|
||||
const allMet = Object.values(PROF_RULES).every(r => r(pw));
|
||||
document.getElementById('prof-pw').classList.toggle('is-valid', allMet);
|
||||
document.getElementById('prof-pw').classList.toggle('is-invalid', !allMet);
|
||||
profConfirmInput();
|
||||
}
|
||||
function profConfirmInput() {
|
||||
const pw = document.getElementById('prof-pw').value;
|
||||
const pw2 = document.getElementById('prof-pw2').value;
|
||||
const msg = document.getElementById('prof-match-msg');
|
||||
const el2 = document.getElementById('prof-pw2');
|
||||
if (!pw2) { msg.innerHTML=''; el2.classList.remove('is-valid','is-invalid'); return; }
|
||||
const ok = pw === pw2;
|
||||
msg.innerHTML = ok
|
||||
? '<span style="color:var(--success);"><i class="bi bi-check-circle-fill me-1"></i>Passwords match</span>'
|
||||
: '<span style="color:var(--danger);"><i class="bi bi-x-circle-fill me-1"></i>Passwords do not match</span>';
|
||||
el2.classList.toggle('is-valid', ok);
|
||||
el2.classList.toggle('is-invalid', !ok);
|
||||
}
|
||||
function profToggle(inputId, btnId) {
|
||||
const inp = document.getElementById(inputId);
|
||||
const btn = document.getElementById(btnId);
|
||||
const isPassword = inp.type === 'password';
|
||||
inp.type = isPassword ? 'text' : 'password';
|
||||
btn.querySelector('i').className = isPassword ? 'bi bi-eye-slash' : 'bi bi-eye';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -335,6 +335,9 @@
|
||||
<li><a href="{{ url_for('admin.kb_list') }}" class="{{ 'active' if 'admin.kb' in request.endpoint }}">
|
||||
<i class="bi bi-journal-text"></i> Manage KB
|
||||
</a></li>
|
||||
<li><a href="{{ url_for('admin.canned_responses') }}" class="{{ 'active' if 'canned_response' in request.endpoint }}">
|
||||
<i class="bi bi-chat-square-text"></i> Quick Replies
|
||||
</a></li>
|
||||
{% if current_user.is_admin %}
|
||||
<li><a href="{{ url_for('admin.users') }}" class="{{ 'active' if request.endpoint == 'admin.users' }}">
|
||||
<i class="bi bi-people"></i> Users
|
||||
|
||||
@@ -179,6 +179,33 @@
|
||||
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.zip,.log"/>
|
||||
</div>
|
||||
{% if current_user.is_it_staff %}
|
||||
<!-- Canned responses picker -->
|
||||
{% if canned_responses %}
|
||||
<div class="mb-3">
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-secondary btn-sm dropdown-toggle"
|
||||
id="canned-responses-btn" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-chat-square-text me-1"></i>Quick Replies
|
||||
</button>
|
||||
<ul class="dropdown-menu" style="max-height:260px;overflow-y:auto;min-width:300px;">
|
||||
{% set ns = namespace(last_cat='') %}
|
||||
{% for r in canned_responses %}
|
||||
{% if r.category and r.category != ns.last_cat %}
|
||||
{% if ns.last_cat %}<li><hr class="dropdown-divider"/></li>{% endif %}
|
||||
<li><span class="dropdown-header" style="font-size:10px;text-transform:uppercase;letter-spacing:.5px;">{{ r.category }}</span></li>
|
||||
{% set ns.last_cat = r.category %}
|
||||
{% endif %}
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" style="font-size:13px;white-space:normal;"
|
||||
onclick="insertCannedResponse({{ r.id }})">
|
||||
{{ r.title }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-3 d-flex align-items-center gap-2">
|
||||
<input type="checkbox" id="is_internal" name="is_internal" style="accent-color:var(--warning);"/>
|
||||
<label for="is_internal" style="font-size:13px;color:var(--warning);margin:0;cursor:pointer;">
|
||||
@@ -196,6 +223,20 @@
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
const CANNED_RESPONSES = {
|
||||
{% for r in canned_responses %}
|
||||
{{ r.id }}: {{ r.body | tojson }},
|
||||
{% endfor %}
|
||||
};
|
||||
function insertCannedResponse(id) {
|
||||
const body = CANNED_RESPONSES[id];
|
||||
if (!body) return;
|
||||
const ta = document.getElementById('comment-body');
|
||||
const cur = ta.value;
|
||||
ta.value = cur ? cur + '\n\n' + body : body;
|
||||
ta.focus();
|
||||
ta.setSelectionRange(ta.value.length, ta.value.length);
|
||||
}
|
||||
const TICKET_ID = {{ ticket.id }};
|
||||
const CURRENT_UID = {{ current_user.id }};
|
||||
const IS_IT_STAFF = {{ 'true' if current_user.is_it_staff else 'false' }};
|
||||
@@ -587,6 +628,69 @@ function buildCommentEl(c) {
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Linked Tickets -->
|
||||
{% if current_user.is_it_staff %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-link-45deg me-2"></i>Linked Tickets</span>
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#link-form-collapse"
|
||||
style="font-size:11px;">
|
||||
<i class="bi bi-plus me-1"></i>Link
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" style="font-size:13px;">
|
||||
{% if linked_tickets %}
|
||||
{% for lnk, other in linked_tickets %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-2"
|
||||
style="padding:6px 8px;background:var(--surface2);border-radius:6px;border:1px solid var(--border);">
|
||||
<div style="min-width:0;">
|
||||
<a href="{{ url_for('tickets.ticket_detail', ticket_id=other.id) }}"
|
||||
class="mono" style="font-size:11px;color:var(--accent3);">{{ other.ticket_number }}</a>
|
||||
<span style="font-size:10px;background:var(--border);color:var(--muted);padding:1px 6px;border-radius:4px;margin-left:4px;">{{ lnk.link_type.replace('_',' ') }}</span>
|
||||
<div style="font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ other.title }}</div>
|
||||
</div>
|
||||
<form method="POST"
|
||||
action="{{ url_for('tickets.unlink_ticket', ticket_id=ticket.id, link_id=lnk.id) }}"
|
||||
onsubmit="return confirm('Remove this link?')" style="margin:0;flex-shrink:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm" title="Remove link"
|
||||
style="background:none;border:none;color:var(--muted);padding:2px 6px;">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color:var(--muted);font-size:12px;margin:0;">No linked tickets yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Link form (collapsed by default) -->
|
||||
<div class="collapse mt-3" id="link-form-collapse">
|
||||
<form method="POST" action="{{ url_for('tickets.link_ticket', ticket_id=ticket.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" style="font-size:12px;">Ticket ID or Number</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
name="linked_ticket_id" placeholder="e.g. 42" required min="1"/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" style="font-size:12px;">Relationship</label>
|
||||
<select class="form-select form-select-sm" name="link_type">
|
||||
<option value="related">Related</option>
|
||||
<option value="duplicate">Duplicate</option>
|
||||
<option value="follow_up">Follow-up</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100">
|
||||
<i class="bi bi-link-45deg me-1"></i>Create Link
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Ticket Info -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><i class="bi bi-info-circle me-2"></i>Ticket Details</div>
|
||||
@@ -647,4 +751,53 @@ function buildCommentEl(c) {
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-open modal trigger — shown to ticket creator when resolved or closed -->
|
||||
{% if ticket.status in ('resolved', 'closed') and
|
||||
(current_user.is_it_staff or ticket.created_by_id == current_user.id) %}
|
||||
<div class="card mt-4" style="border-color:var(--warning);max-width:760px;">
|
||||
<div class="card-body d-flex align-items-center gap-3" style="padding:16px 20px;">
|
||||
<i class="bi bi-arrow-repeat" style="font-size:22px;color:var(--warning);flex-shrink:0;"></i>
|
||||
<div style="flex:1;">
|
||||
<div style="font-weight:600;font-size:14px;">Has this issue returned?</div>
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:2px;">
|
||||
Re-open this ticket to notify IT staff and resume tracking.
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm"
|
||||
data-bs-toggle="modal" data-bs-target="#reopen-modal">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>Re-open Ticket
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-open modal -->
|
||||
<div class="modal fade" id="reopen-modal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-arrow-repeat me-2"></i>Re-open Ticket</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('tickets.reopen_ticket', ticket_id=ticket.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="modal-body">
|
||||
<p style="font-size:13px;color:var(--muted);">
|
||||
Please describe why the issue has returned. This will be posted as a comment
|
||||
and IT staff will be notified immediately.
|
||||
</p>
|
||||
<textarea class="form-control" name="reopen_reason" rows="4" required
|
||||
placeholder="e.g. The problem came back after the weekend restart. The error message is now different: …"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>Re-open & Notify IT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user