Code reviewed and issue fixed.

This commit is contained in:
2026-03-27 14:01:11 -04:00
parent d8828d487a
commit 359c801027
10 changed files with 788 additions and 113 deletions
+237 -44
View File
@@ -3,15 +3,16 @@
{% block page_title %}IT Operations Overview{% endblock %}
{% block content %}
<!-- Stats grid -->
{# ── Headline stat cards ───────────────────────────────────────────────────── #}
<div class="row g-3 mb-4">
{% for label, value, icon, color, bg in [
('Total Tickets', stats.total_tickets, 'bi-collection', 'var(--text)', 'rgba(255,255,255,.05)'),
('Open', stats.open, 'bi-circle', 'var(--info)', 'rgba(96,165,250,.1)'),
('In Progress', stats.in_progress, 'bi-arrow-repeat', 'var(--accent3)', 'rgba(0,180,216,.1)'),
('Resolved', stats.resolved, 'bi-check-circle', 'var(--success)', 'rgba(45,212,191,.1)'),
('Closed', stats.closed, 'bi-archive', 'var(--muted)', 'rgba(112,112,160,.1)'),
('Active Users', stats.total_users, 'bi-people', 'var(--accent2)', 'rgba(123,94,167,.1)'),
('Total Tickets', stats.total_tickets, 'bi-collection', 'var(--text)', 'rgba(255,255,255,.05)'),
('Open', stats.open, 'bi-circle', 'var(--info)', 'rgba(96,165,250,.1)'),
('In Progress', stats.in_progress, 'bi-arrow-repeat','var(--accent3)','rgba(0,180,216,.1)'),
('Resolved', stats.resolved, 'bi-check-circle','var(--success)','rgba(45,212,191,.1)'),
('Closed', stats.closed, 'bi-archive', 'var(--muted)', 'rgba(112,112,160,.1)'),
('Active Users', stats.total_users, 'bi-people', 'var(--accent2)','rgba(123,94,167,.1)'),
] %}
<div class="col-6 col-xl-2">
<div class="stat-card">
@@ -25,44 +26,236 @@
{% endfor %}
</div>
<!-- Activity log -->
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between">
<span><i class="bi bi-activity me-2"></i>Recent Activity</span>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.activity_logs') }}" style="font-size:12px;color:var(--accent3);">Full log →</a>
{% endif %}
{# ── Row 1: Performance KPIs + Priority breakdown ─────────────────────────── #}
<div class="row g-3 mb-4">
{# KPI cards #}
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header"><i class="bi bi-speedometer2 me-2"></i>IT Performance — Last 30 Days</div>
<div class="card-body">
<div class="row g-3">
{# Avg resolution time #}
<div class="col-6 col-lg-3">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;
color:{% if avg_resolution_hours is none %}var(--muted)
{% elif avg_resolution_hours <= 24 %}var(--success)
{% elif avg_resolution_hours <= 72 %}var(--warning)
{% else %}var(--danger){% endif %};">
{% if avg_resolution_hours is none %}—
{% elif avg_resolution_hours >= 24 %}{{ (avg_resolution_hours / 24)|round(1) }}d
{% else %}{{ avg_resolution_hours }}h{% endif %}
</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">Avg Resolution Time</div>
</div>
</div>
{# Resolved last 7d #}
<div class="col-6 col-lg-3">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;color:var(--success);">{{ resolved_7d }}</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">Resolved (7 days)</div>
</div>
</div>
{# New last 7d #}
<div class="col-6 col-lg-3">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;color:var(--info);">{{ new_7d }}</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">New (7 days)</div>
</div>
</div>
{# Resolution rate 30d #}
<div class="col-6 col-lg-3">
{% set rate = ((resolved_30d / new_30d * 100)|int) if new_30d > 0 else 0 %}
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;
color:{% if rate >= 80 %}var(--success){% elif rate >= 50 %}var(--warning){% else %}var(--danger){% endif %};">
{{ rate }}%
</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">Resolution Rate (30d)</div>
</div>
</div>
{# Unassigned #}
<div class="col-6 col-lg-3">
<a href="{{ url_for('admin.all_tickets', assigned='unassigned') }}" style="text-decoration:none;">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;cursor:pointer;">
<div style="font-size:26px;font-weight:700;color:{% if unassigned > 0 %}var(--warning){% else %}var(--success){% endif %};">
{{ unassigned }}
</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">Unassigned Open</div>
</div>
</a>
</div>
{# SLA at-risk #}
<div class="col-6 col-lg-3">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;color:{% if sla_breach > 0 %}var(--danger){% else %}var(--success){% endif %};">
{{ sla_breach }}
</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">SLA At-Risk (&gt;3 days)</div>
</div>
</div>
{# Resolved 30d #}
<div class="col-6 col-lg-3">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;color:var(--success);">{{ resolved_30d }}</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">Resolved (30 days)</div>
</div>
</div>
{# New 30d #}
<div class="col-6 col-lg-3">
<div style="text-align:center;padding:12px;background:var(--surface2);border-radius:10px;">
<div style="font-size:26px;font-weight:700;color:var(--info);">{{ new_30d }}</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px;">New (30 days)</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body p-0">
<table class="table mb-0">
<thead>
<tr>
<th>Action</th>
<th>User</th>
<th>Entity</th>
<th>Details</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{% for log in recent_logs %}
<tr>
<td>
<span class="mono" style="font-size:11px;
color:{% if 'create' in log.action %}var(--success){% elif 'delete' in log.action %}var(--danger){% elif 'update' in log.action or 'edit' in log.action %}var(--warning){% else %}var(--muted){% endif %};">
{{ log.action }}
</span>
</td>
<td style="font-size:13px;">
{% if log.user %}{{ log.user.full_name }}{% else %}<span style="color:var(--muted);">System</span>{% endif %}
</td>
<td style="font-size:12px;color:var(--muted);">{{ log.entity_type or '—' }} {% if log.entity_id %}#{{ log.entity_id }}{% endif %}</td>
<td style="font-size:12px;color:var(--muted);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ log.details or '—' }}</td>
<td style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;">{{ log.created_at.strftime('%b %d %H:%M') }}</td>
</tr>
{# Priority breakdown #}
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header"><i class="bi bi-bar-chart me-2"></i>Open by Priority</div>
<div class="card-body">
{% set total_open = priority_breakdown.values()|sum %}
{% for priority, count in [
('critical', priority_breakdown.critical),
('high', priority_breakdown.high),
('medium', priority_breakdown.medium),
('low', priority_breakdown.low),
] %}
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="badge badge-{{ priority }}" style="font-size:11px;">{{ priority.upper() }}</span>
<span style="font-size:13px;font-weight:600;">{{ count }}</span>
</div>
<div style="height:6px;background:var(--surface2);border-radius:3px;overflow:hidden;">
<div style="height:100%;width:{{ ((count / total_open * 100)|int) if total_open > 0 else 0 }}%;
background:{% if priority=='critical' %}#374151{% elif priority=='high' %}var(--danger){% elif priority=='medium' %}var(--warning){% else %}var(--success){% endif %};
border-radius:3px;transition:width .4s;"></div>
</div>
</div>
{% endfor %}
</tbody>
</table>
<hr style="border-color:var(--border);margin:12px 0;">
<div style="font-size:12px;color:var(--muted);">Open by Category</div>
<div class="mt-2">
{% for cat, count in category_breakdown %}
{% if count > 0 %}
<div class="d-flex justify-content-between" style="font-size:12px;padding:3px 0;border-bottom:1px solid var(--border);">
<span style="color:var(--muted);">{{ cat }}</span>
<span style="font-weight:600;">{{ count }}</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{# ── Row 2: Staff performance + Urgent queue ──────────────────────────────── #}
<div class="row g-3">
{# Staff performance table #}
<div class="col-lg-7">
<div class="card">
<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 %}
<table class="table mb-0">
<thead>
<tr>
<th>Staff Member</th>
<th style="text-align:center;">Open</th>
<th style="text-align:center;">Urgent</th>
<th style="text-align:center;">Resolved (30d)</th>
<th style="text-align:center;">Avg Time</th>
<th style="text-align:center;">Total Resolved</th>
</tr>
</thead>
<tbody>
{% for s in staff_stats %}
<tr>
<td style="font-size:13px;font-weight:500;">{{ s.name }}</td>
<td style="text-align:center;">
<span style="font-size:13px;font-weight:600;color:{% if s.assigned_open > 5 %}var(--danger){% elif s.assigned_open > 2 %}var(--warning){% else %}var(--success){% endif %};">
{{ s.assigned_open }}
</span>
</td>
<td style="text-align:center;">
{% if s.urgent_open > 0 %}
<span style="font-size:12px;font-weight:600;color:var(--danger);">{{ s.urgent_open }}</span>
{% else %}
<span style="font-size:12px;color:var(--success);"><i class="bi bi-check2"></i></span>
{% endif %}
</td>
<td style="text-align:center;font-size:13px;color:var(--success);font-weight:600;">{{ s.resolved_30d }}</td>
<td style="text-align:center;font-size:12px;color:var(--muted);">
{% if s.avg_hours is none %}—
{% elif s.avg_hours >= 24 %}{{ (s.avg_hours / 24)|round(1) }}d
{% else %}{{ s.avg_hours }}h{% endif %}
</td>
<td style="text-align:center;font-size:12px;color:var(--muted);">{{ s.resolved_total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-4 text-center" style="color:var(--muted);font-size:13px;">No IT staff found.</div>
{% endif %}
</div>
</div>
</div>
{# Urgent open tickets queue #}
<div class="col-lg-5">
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between">
<span><i class="bi bi-exclamation-triangle me-2"></i>Needs Attention</span>
<a href="{{ url_for('admin.all_tickets', status='open') }}" style="font-size:12px;color:var(--accent3);">View all →</a>
</div>
<div class="card-body p-0">
{% if urgent_tickets %}
{% for t in urgent_tickets %}
<a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}"
style="display:flex;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid var(--border);color:var(--text);text-decoration:none;">
<span class="badge badge-{{ t.priority }}" style="flex-shrink:0;font-size:10px;">{{ t.priority.upper() }}</span>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ t.title }}</div>
<div style="font-size:11px;color:var(--muted);">
{{ t.ticket_number }} · {{ t.creator.full_name.split()[0] }} ·
{% set age = ((now - t.created_at).total_seconds() / 3600)|int %}
{% if age >= 24 %}{{ (age / 24)|int }}d ago{% else %}{{ age }}h ago{% endif %}
</div>
</div>
{% if t.assignee %}
<span style="font-size:11px;color:var(--muted);flex-shrink:0;">{{ t.assignee.full_name.split()[0] }}</span>
{% else %}
<span style="font-size:11px;color:var(--warning);flex-shrink:0;font-style:italic;">Unassigned</span>
{% endif %}
</a>
{% endfor %}
{% else %}
<div class="p-4 text-center" style="color:var(--success);font-size:13px;">
<i class="bi bi-check2-all" style="font-size:28px;display:block;margin-bottom:8px;"></i>
All caught up — no urgent open tickets.
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}