05/22 Enhance codes and fix bugs 3
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
<div class="col-6 col-md-4">
|
||||
<button type="button" class="btn btn-secondary w-100 text-start template-btn"
|
||||
style="padding:10px 14px;font-size:13px;"
|
||||
data-name="{{ t.name }}"
|
||||
data-title="{{ t.title_hint }}"
|
||||
data-category="{{ t.category }}"
|
||||
data-priority="{{ t.priority }}"
|
||||
@@ -26,6 +27,23 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Template preview panel -->
|
||||
<div id="tmpl-preview" style="display:none;margin-top:14px;padding:14px 16px;
|
||||
background:var(--surface2);border:1px solid var(--border);border-radius:8px;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
|
||||
<span style="font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;">Preview</span>
|
||||
<button type="button" id="tmpl-apply-btn" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-check2 me-1"></i>Apply Template
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:4px;" id="tmpl-prev-title">—</div>
|
||||
<div style="display:flex;gap:6px;margin-bottom:8px;">
|
||||
<span style="font-size:11px;padding:1px 8px;border-radius:4px;background:var(--border);color:var(--muted);" id="tmpl-prev-cat">—</span>
|
||||
<span id="tmpl-prev-prio" class="badge">—</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted);white-space:pre-wrap;line-height:1.55;max-height:100px;overflow-y:auto;" id="tmpl-prev-desc">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -135,33 +153,60 @@
|
||||
</div>
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelectorAll('.template-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const title = btn.dataset.title;
|
||||
const cat = btn.dataset.category;
|
||||
const prio = btn.dataset.priority;
|
||||
const desc = btn.dataset.description;
|
||||
(function () {
|
||||
const preview = document.getElementById('tmpl-preview');
|
||||
const applyBtn = document.getElementById('tmpl-apply-btn');
|
||||
const prevTitle = document.getElementById('tmpl-prev-title');
|
||||
const prevCat = document.getElementById('tmpl-prev-cat');
|
||||
const prevPrio = document.getElementById('tmpl-prev-prio');
|
||||
const prevDesc = document.getElementById('tmpl-prev-desc');
|
||||
|
||||
if (title) document.querySelector('input[name=title]').value = title;
|
||||
if (desc) document.querySelector('textarea[name=description]').value = desc;
|
||||
let activeData = null;
|
||||
|
||||
const catSel = document.querySelector('select[name=category]');
|
||||
if (catSel) {
|
||||
for (const opt of catSel.options) {
|
||||
if (opt.value === cat) { opt.selected = true; break; }
|
||||
}
|
||||
}
|
||||
function applyTemplate(data) {
|
||||
if (!data) return;
|
||||
if (data.title) document.querySelector('input[name=title]').value = data.title;
|
||||
if (data.desc) document.querySelector('textarea[name=description]').value = data.desc;
|
||||
const catSel = document.querySelector('select[name=category]');
|
||||
const prioSel = document.querySelector('select[name=priority]');
|
||||
if (prioSel) {
|
||||
for (const opt of prioSel.options) {
|
||||
if (opt.value === prio) { opt.selected = true; break; }
|
||||
}
|
||||
}
|
||||
// Scroll to form
|
||||
if (catSel) { for (const o of catSel.options) { if (o.value === data.cat) { o.selected = true; break; } } }
|
||||
if (prioSel) { for (const o of prioSel.options) { if (o.value === data.prio) { o.selected = true; break; } } }
|
||||
document.querySelector('input[name=title]').scrollIntoView({behavior:'smooth', block:'center'});
|
||||
document.querySelector('input[name=title]').focus();
|
||||
// Highlight briefly
|
||||
document.querySelector('input[name=title]').style.outline = '2px solid var(--accent)';
|
||||
setTimeout(() => { document.querySelector('input[name=title]').style.outline = ''; }, 1200);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.template-btn').forEach(btn => {
|
||||
function showPreview() {
|
||||
activeData = {
|
||||
name: btn.dataset.name,
|
||||
title: btn.dataset.title,
|
||||
cat: btn.dataset.category,
|
||||
prio: btn.dataset.priority,
|
||||
desc: btn.dataset.description,
|
||||
};
|
||||
prevTitle.textContent = activeData.title || activeData.name;
|
||||
prevCat.textContent = (activeData.cat || '').replace(/_/g, ' ');
|
||||
prevDesc.textContent = activeData.desc || '(no description)';
|
||||
prevPrio.textContent = (activeData.prio || '').toUpperCase();
|
||||
prevPrio.className = 'badge badge-' + (activeData.prio || 'medium');
|
||||
preview.style.display = 'block';
|
||||
}
|
||||
|
||||
btn.addEventListener('mouseenter', showPreview);
|
||||
btn.addEventListener('focus', showPreview);
|
||||
btn.addEventListener('click', function () {
|
||||
showPreview();
|
||||
applyTemplate(activeData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (applyBtn) {
|
||||
applyBtn.addEventListener('click', () => applyTemplate(activeData));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -811,11 +811,103 @@ function buildCommentEl(c) {
|
||||
{{ info_row('bi-person-check', 'Assigned to', ticket.assignee.full_name if ticket.assignee else '—') }}
|
||||
{{ info_row('bi-calendar3', 'Created', ticket.created_at | localtime("%b %d, %Y")) }}
|
||||
{{ info_row('bi-calendar-check', 'Updated', ticket.updated_at | localtime("%b %d, %Y")) }}
|
||||
{% if ticket.due_date %}{{ info_row('bi-alarm', 'Due Date', ticket.due_date | localtime("%b %d, %Y")) }}{% endif %}
|
||||
{% if ticket.due_date %}
|
||||
{% set is_done = ticket.status in ('resolved', 'closed') %}
|
||||
{% set overdue = not is_done and ticket.due_date < now %}
|
||||
{% set due_str = ticket.due_date | localtime("%b %d, %Y") %}
|
||||
{% if is_done %}
|
||||
{% set sla_badge = '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(5,150,105,.12);color:var(--success);font-weight:600;margin-left:6px;">Completed</span>' %}
|
||||
{% elif overdue %}
|
||||
{% set sla_badge = '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(220,38,38,.12);color:var(--danger);font-weight:600;margin-left:6px;"><i class=\"bi bi-exclamation-triangle-fill\"></i> Overdue</span>' %}
|
||||
{% else %}
|
||||
{% set sla_badge = '<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(5,150,105,.12);color:var(--success);font-weight:600;margin-left:6px;">On track</span>' %}
|
||||
{% endif %}
|
||||
{{ info_row('bi-alarm', 'SLA Due', due_str ~ sla_badge) }}
|
||||
{% endif %}
|
||||
{% if ticket.resolved_at %}{{ info_row('bi-check-circle', 'Resolved', ticket.resolved_at | localtime("%b %d, %Y")) }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Watch / Unwatch -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2 px-3 d-flex align-items-center justify-content-between">
|
||||
<span style="font-size:13px;color:var(--muted);">
|
||||
<i class="bi bi-eye me-1"></i>
|
||||
<span id="watch-label">{% if is_watching %}Watching this ticket{% else %}Not watching{% endif %}</span>
|
||||
</span>
|
||||
<button id="watch-btn" class="btn btn-sm {% if is_watching %}btn-secondary{% else %}btn-outline-secondary{% endif %}"
|
||||
style="font-size:12px;"
|
||||
onclick="toggleWatch({{ ticket.id }}, {{ 'true' if is_watching else 'false' }})">
|
||||
<i class="bi {% if is_watching %}bi-eye-slash{% else %}bi-eye{% endif %}" id="watch-icon"></i>
|
||||
<span id="watch-btn-text">{% if is_watching %}Unwatch{% else %}Watch{% endif %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Tracking (IT staff only) -->
|
||||
{% 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-clock me-2"></i>Time Logged</span>
|
||||
<span style="font-size:12px;color:var(--muted);" id="total-time-display">
|
||||
{{ (total_minutes // 60) }}h {{ (total_minutes % 60) }}m total
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="time-log-form" class="d-flex gap-2 align-items-end mb-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div style="flex:0 0 80px;">
|
||||
<label class="form-label" style="font-size:11px;margin-bottom:3px;">Minutes</label>
|
||||
<input type="number" name="minutes" min="1" max="480" class="form-control form-control-sm"
|
||||
placeholder="30" id="time-minutes-input"/>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="form-label" style="font-size:11px;margin-bottom:3px;">Note (optional)</label>
|
||||
<input type="text" name="note" maxlength="200" class="form-control form-control-sm"
|
||||
placeholder="e.g. Investigated logs"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Log</button>
|
||||
</form>
|
||||
{% if time_entries %}
|
||||
<div style="font-size:12px;">
|
||||
{% for e in time_entries %}
|
||||
<div class="d-flex justify-content-between py-1" style="border-top:1px solid var(--border);">
|
||||
<span style="color:var(--muted);">{{ e.user.full_name.split()[0] }} — {{ e.note or '—' }}</span>
|
||||
<span style="font-weight:600;white-space:nowrap;">{{ e.minutes }}m</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const form = document.getElementById('time-log-form');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const data = new FormData(form);
|
||||
fetch('{{ url_for("tickets.log_time", ticket_id=ticket.id) }}', {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content},
|
||||
body: data,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.ok) {
|
||||
const h = Math.floor(d.total_minutes / 60), m = d.total_minutes % 60;
|
||||
document.getElementById('total-time-display').textContent = h + 'h ' + m + 'm total';
|
||||
form.reset();
|
||||
showToast('Time logged', d.minutes + ' minutes logged successfully.');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('Error', 'Could not log time. Please try again.'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- History -->
|
||||
<!-- ── Satisfaction rating (visible to creator + IT staff) ──────── -->
|
||||
{% if ticket.satisfaction %}
|
||||
@@ -1050,4 +1142,28 @@ function buildCommentEl(c) {
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleWatch(ticketId, currently) {
|
||||
const action = currently ? 'unwatch' : 'watch';
|
||||
fetch('/tickets/' + ticketId + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const nowWatching = d.watching;
|
||||
document.getElementById('watch-label').textContent = nowWatching ? 'Watching this ticket' : 'Not watching';
|
||||
document.getElementById('watch-icon').className = nowWatching ? 'bi bi-eye-slash' : 'bi bi-eye';
|
||||
document.getElementById('watch-btn-text').textContent = nowWatching ? 'Unwatch' : 'Watch';
|
||||
const btn = document.getElementById('watch-btn');
|
||||
btn.className = 'btn btn-sm ' + (nowWatching ? 'btn-secondary' : 'btn-outline-secondary');
|
||||
btn.setAttribute('onclick', 'toggleWatch(' + ticketId + ', ' + (nowWatching ? 'true' : 'false') + ')');
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user