05/22 Enhance codes and fix bugs 2
This commit is contained in:
@@ -134,7 +134,7 @@ def dashboard():
|
||||
return render_template('tickets/dashboard_employee.html',
|
||||
my_tickets=my_tickets, open_count=open_count,
|
||||
active_count=active_count, resolved_count=resolved_count,
|
||||
articles=articles,
|
||||
articles=articles, now=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
@@ -538,6 +538,12 @@ def update_ticket(ticket_id):
|
||||
if new_status == TicketStatus.RESOLVED:
|
||||
send_satisfaction_survey(ticket)
|
||||
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
from flask import jsonify
|
||||
return jsonify({'ok': True, 'changes': changes,
|
||||
'status': ticket.status, 'priority': ticket.priority,
|
||||
'assigned_to_id': ticket.assigned_to_id})
|
||||
|
||||
flash('Ticket updated successfully.', 'success')
|
||||
return redirect(url_for('tickets.ticket_detail', ticket_id=ticket.id))
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -143,6 +144,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if logs.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
<div class="card-header"><i class="bi bi-people me-2"></i>IT Staff Performance</div>
|
||||
<div class="card-body p-0">
|
||||
{% if staff_stats %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -212,6 +213,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center" style="color:var(--muted);font-size:13px;">No IT staff found.</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if articles %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr><th>Title</th><th>Category</th><th>Author</th><th>Published</th><th>Views</th><th style="white-space:nowrap;">👍 / 👎</th><th>Updated</th><th>Actions</th></tr>
|
||||
@@ -120,6 +121,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-journal-x" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if templates %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -68,6 +69,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-layout-text-window-reverse" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if tickets.items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -115,6 +116,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if tickets.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -73,6 +74,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+102
-1
@@ -2,6 +2,9 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<script>
|
||||
(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-bs-theme',t);})();
|
||||
</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
@@ -52,6 +55,36 @@
|
||||
--shadow-lg: 0 10px 25px rgba(0, 0, 0, .1), 0 4px 10px rgba(0, 0, 0, .06);
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface2: #1c2333;
|
||||
--border: #30363d;
|
||||
--border2: #3d444d;
|
||||
--text: #e6edf3;
|
||||
--text2: #adbac7;
|
||||
--muted: #8b949e;
|
||||
--muted2: #545d68;
|
||||
--success: #3fb950;
|
||||
--success-bg: #0b2a12;
|
||||
--warning: #d29922;
|
||||
--warning-bg: #1c1100;
|
||||
--danger: #f85149;
|
||||
--danger-bg: #210b0a;
|
||||
--info: #388bfd;
|
||||
--info-bg: #031d40;
|
||||
--sidebar-bg: #0d1117;
|
||||
--sidebar-text: #8b949e;
|
||||
--sidebar-active: #e6edf3;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.25);
|
||||
--shadow: 0 4px 6px rgba(0,0,0,.3), 0 2px 4px rgba(0,0,0,.2);
|
||||
--shadow-lg: 0 10px 25px rgba(0,0,0,.5), 0 4px 10px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] #topbar {
|
||||
background: rgba(22, 27, 34, .95);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -1181,6 +1214,10 @@
|
||||
{{ unread_notifications if unread_notifications > 0 }}
|
||||
</span>
|
||||
</button>
|
||||
<!-- Dark mode toggle -->
|
||||
<button id="dark-toggle" class="btn btn-sm btn-secondary" title="Toggle dark mode" onclick="toggleDarkMode()" style="padding:4px 8px;">
|
||||
<i class="bi bi-moon" id="dark-icon"></i>
|
||||
</button>
|
||||
<a href="{{ url_for('auth.profile') }}" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-person-circle me-1"></i>{{ current_user.full_name.split()[0] }}
|
||||
</a>
|
||||
@@ -1513,7 +1550,7 @@
|
||||
|
||||
function showToast(title, msg) {
|
||||
const t = document.createElement('div');
|
||||
t.style.cssText = 'position:fixed;bottom:90px;right:28px;background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent3);border-radius:8px;padding:12px 16px;z-index:300;max-width:300px;box-shadow:0 4px 16px rgba(0,0,0,.4);animation:slideIn .25s ease;';
|
||||
t.style.cssText = 'position:fixed;top:72px;right:16px;background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent3);border-radius:8px;padding:12px 16px;z-index:1090;max-width:320px;box-shadow:0 4px 16px rgba(0,0,0,.18);animation:slideIn .25s ease;';
|
||||
t.innerHTML = `<div style="font-size:13px;font-weight:600;margin-bottom:4px;">${_esc(title)}</div><div style="font-size:12px;color:var(--muted);">${_esc(msg)}</div>`;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 5000);
|
||||
@@ -1591,7 +1628,71 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#nprogress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
width: 0%;
|
||||
background: var(--accent);
|
||||
z-index: 9999;
|
||||
transition: width .2s ease, opacity .4s ease;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'nprogress-bar';
|
||||
document.body.appendChild(bar);
|
||||
|
||||
let _raf;
|
||||
function startProgress() {
|
||||
bar.style.opacity = '1';
|
||||
bar.style.width = '0%';
|
||||
bar.style.transition = 'width .2s ease';
|
||||
let w = 0;
|
||||
function tick() {
|
||||
w = w < 70 ? w + (70 - w) * 0.08 : w < 90 ? w + 0.4 : w;
|
||||
bar.style.width = Math.min(w, 90) + '%';
|
||||
_raf = requestAnimationFrame(tick);
|
||||
}
|
||||
_raf = requestAnimationFrame(tick);
|
||||
}
|
||||
function finishProgress() {
|
||||
cancelAnimationFrame(_raf);
|
||||
bar.style.width = '100%';
|
||||
bar.style.transition = 'width .15s ease, opacity .4s ease .15s';
|
||||
setTimeout(() => { bar.style.opacity = '0'; setTimeout(() => { bar.style.width = '0%'; }, 400); }, 200);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
const a = e.target.closest('a[href]');
|
||||
if (!a) return;
|
||||
const href = a.getAttribute('href');
|
||||
if (!href || href.startsWith('#') || href.startsWith('javascript') || a.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||
try { const u = new URL(href, location.href); if (u.origin !== location.origin) return; } catch (_) { return; }
|
||||
startProgress();
|
||||
});
|
||||
window.addEventListener('pageshow', finishProgress);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
function toggleDarkMode() {
|
||||
const html = document.documentElement;
|
||||
const next = html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-bs-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
_syncDarkIcon(next);
|
||||
}
|
||||
function _syncDarkIcon(theme) {
|
||||
const icon = document.getElementById('dark-icon');
|
||||
if (!icon) return;
|
||||
icon.className = theme === 'dark' ? 'bi bi-sun' : 'bi bi-moon';
|
||||
}
|
||||
_syncDarkIcon(document.documentElement.getAttribute('data-bs-theme') || 'light');
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
|
||||
@@ -52,28 +52,64 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if my_tickets %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket #</th>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Priority</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in my_tickets %}
|
||||
{% set is_done = t.status in ('resolved', 'closed') %}
|
||||
{% set is_overdue = t.due_date and not is_done and t.due_date < now %}
|
||||
{% set stage = 0 %}
|
||||
{% if t.status == 'in_progress' %}{% set stage = 1 %}{% endif %}
|
||||
{% if t.status == 'pending' %}{% set stage = 2 %}{% endif %}
|
||||
{% if is_done %}{% set stage = 3 %}{% endif %}
|
||||
<tr style="cursor:pointer;" onclick="window.location='/tickets/{{ t.id }}'">
|
||||
<td><span class="mono" style="font-size:12px;color:var(--accent3);">{{ t.ticket_number }}</span></td>
|
||||
<td>{{ t.title[:45] }}{% if t.title|length > 45 %}…{% endif %}</td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status.replace('_',' ').upper() }}</span></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<span class="mono" style="font-size:12px;color:var(--accent3);">{{ t.ticket_number }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="font-size:13px;">
|
||||
{{ t.title[:40] }}{% if t.title|length > 40 %}…{% endif %}
|
||||
</div>
|
||||
<div style="margin-top:3px;display:flex;gap:4px;flex-wrap:wrap;">
|
||||
{% if is_overdue %}
|
||||
<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(220,38,38,.12);color:var(--danger);font-weight:600;">
|
||||
<i class="bi bi-exclamation-triangle-fill" style="font-size:9px;"></i> Overdue
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if t.priority == 'critical' %}
|
||||
<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(220,38,38,.12);color:var(--danger);font-weight:600;">Critical</span>
|
||||
{% elif t.priority == 'high' %}
|
||||
<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:rgba(217,119,6,.12);color:var(--warning);font-weight:600;">High</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td style="min-width:110px;">
|
||||
<div style="font-size:10px;color:var(--muted);margin-bottom:4px;">
|
||||
{{ t.status.replace('_',' ').title() }}
|
||||
</div>
|
||||
<div style="display:flex;gap:2px;">
|
||||
{% for i in range(4) %}
|
||||
<div style="height:4px;flex:1;border-radius:2px;
|
||||
background:{% if i <= stage %}{% if is_overdue %}var(--danger){% elif is_done %}var(--success){% else %}var(--accent){% endif %}{% else %}var(--border2){% endif %};"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at | localtime("%b %d") }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);white-space:nowrap;">{{ t.created_at | localtime("%b %d") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-inbox" style="font-size:36px;display:block;margin-bottom:12px;"></i>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if my_tickets %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Ticket #</th><th>Title</th><th>Priority</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -51,6 +52,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center" style="color:var(--muted);font-size:13px;"><i class="bi bi-inbox" style="font-size:28px;display:block;margin-bottom:8px;"></i>No tickets assigned to you.</div>
|
||||
{% endif %}
|
||||
@@ -66,6 +68,7 @@
|
||||
<a href="{{ url_for('admin.all_tickets') }}" style="font-size:12px;color:var(--accent3);">All tickets →</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Ticket #</th><th>User</th><th>Priority</th><th>Status</th><th>Assignee</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -86,6 +89,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -579,7 +579,7 @@ function buildCommentEl(c) {
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><i class="bi bi-pencil-square me-2"></i>Update Ticket</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('tickets.update_ticket', ticket_id=ticket.id) }}">
|
||||
<form id="ticket-update-form" method="POST" action="{{ url_for('tickets.update_ticket', ticket_id=ticket.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Status</label>
|
||||
@@ -623,10 +623,55 @@ function buildCommentEl(c) {
|
||||
<textarea class="form-control" name="internal_notes" rows="2"
|
||||
placeholder="Private notes for IT team…">{{ ticket.internal_notes or '' }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-check2 me-2"></i>Save Changes
|
||||
</button>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="submit" id="update-save-btn" class="btn btn-primary flex-grow-1">
|
||||
<i class="bi bi-check2 me-2"></i>Save Changes
|
||||
</button>
|
||||
<span id="update-feedback" style="font-size:12px;display:none;"></span>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
(function() {
|
||||
const form = document.getElementById('ticket-update-form');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('update-save-btn');
|
||||
const fb = document.getElementById('update-feedback');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
|
||||
fb.style.display = 'none';
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content },
|
||||
body: new FormData(form)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check2 me-2"></i>Save Changes';
|
||||
if (data.ok) {
|
||||
fb.textContent = '✓ Saved';
|
||||
fb.style.color = 'var(--success)';
|
||||
fb.style.display = 'inline';
|
||||
setTimeout(() => { fb.style.display = 'none'; }, 3000);
|
||||
} else {
|
||||
fb.textContent = 'Error saving';
|
||||
fb.style.color = 'var(--danger)';
|
||||
fb.style.display = 'inline';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check2 me-2"></i>Save Changes';
|
||||
fb.textContent = 'Network error';
|
||||
fb.style.color = 'var(--danger)';
|
||||
fb.style.display = 'inline';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -728,6 +773,24 @@ function buildCommentEl(c) {
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Linked Tickets (read-only for employees) -->
|
||||
{% if not current_user.is_it_staff and linked_tickets %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><i class="bi bi-link-45deg me-2"></i>Linked Tickets</div>
|
||||
<div class="card-body" style="font-size:13px;">
|
||||
{% for lnk, other in linked_tickets %}
|
||||
<div class="d-flex align-items-center gap-2 mb-2"
|
||||
style="padding:6px 8px;background:var(--surface2);border-radius:6px;border:1px solid var(--border);">
|
||||
<a href="{{ url_for('tickets.ticket_detail', ticket_id=other.id) }}"
|
||||
class="mono" style="font-size:11px;color:var(--accent3);flex-shrink:0;">{{ other.ticket_number }}</a>
|
||||
<span style="font-size:10px;background:var(--border);color:var(--muted);padding:1px 6px;border-radius:4px;flex-shrink:0;">{{ lnk.link_type.replace('_',' ') }}</span>
|
||||
<div style="font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;">{{ other.title }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if tickets.items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -103,6 +104,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if tickets.pages > 1 %}
|
||||
|
||||
Reference in New Issue
Block a user