From 0d8edc9d34d4b3fa6ac7cea4a9aac06881990db1 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 4 Apr 2026 12:04:41 -0400 Subject: [PATCH] feat: trend dots on stat cards, team workload panel, stat model improvement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard stat cards now show lt-dot trend indicators (up/warn/idle) based on created_today vs closed_today flow — no extra DB query needed - Add collapsible Team Workload panel showing assignee open ticket counts with progress bars (green/cyan/red by load), avatar, and name - StatsModel.getTicketsByAssignee() now returns proper objects with user_id, display_name, open_count (was name-keyed flat array); limit raised to 8 Co-Authored-By: Claude Sonnet 4.6 --- assets/css/dashboard.css | 17 ++++++++ models/StatsModel.php | 17 +++++--- views/DashboardView.php | 84 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 33edadc..1255031 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -324,6 +324,23 @@ kbd { .lt-stats-grid { grid-template-columns: 1fr; } } +/* ── Team Workload Panel ─────────────────────────────────────── */ +.workload-panel summary { user-select: none; } +.workload-panel[open] summary span:first-child { display: inline-block; transform: rotate(90deg); } +.workload-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.5rem; +} +.workload-item { + display: flex; + align-items: center; + gap: 0.5rem; +} +.workload-info { flex: 1; min-width: 0; } +.workload-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.workload-count { min-width: 1.5rem; text-align: right; flex-shrink: 0; } + /* Loading overlay — wraps lt-spinner for element-level loading states */ .has-lt-overlay { position: relative; } .lt-loading-overlay { diff --git a/models/StatsModel.php b/models/StatsModel.php index 3e867ea..f173c03 100644 --- a/models/StatsModel.php +++ b/models/StatsModel.php @@ -25,16 +25,17 @@ class StatsModel { /** * Get tickets by assignee (top 5) */ - public function getTicketsByAssignee(int $limit = 5): array { + public function getTicketsByAssignee(int $limit = 8): array { $sql = "SELECT + u.user_id, u.display_name, u.username, - COUNT(t.ticket_id) as ticket_count + COUNT(t.ticket_id) as open_count FROM tickets t LEFT JOIN users u ON t.assigned_to = u.user_id - WHERE t.status != 'Closed' + WHERE t.status != 'Closed' AND t.assigned_to IS NOT NULL GROUP BY t.assigned_to - ORDER BY ticket_count DESC + ORDER BY open_count DESC LIMIT ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param('i', $limit); @@ -42,8 +43,12 @@ class StatsModel { $result = $stmt->get_result(); $data = []; while ($row = $result->fetch_assoc()) { - $name = $row['display_name'] ?: $row['username']; - $data[$name] = (int)$row['ticket_count']; + $data[] = [ + 'user_id' => (int)$row['user_id'], + 'display_name' => $row['display_name'] ?: $row['username'], + 'username' => $row['username'], + 'open_count' => (int)$row['open_count'], + ]; } $stmt->close(); return $data; diff --git a/views/DashboardView.php b/views/DashboardView.php index 0765e9b..28cbbe4 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -82,12 +82,26 @@ include __DIR__ . '/layout_header.php';
+ $stats['created_today']) ? 'lt-dot-up' : + ($stats['created_today'] > $stats['closed_today'] ? 'lt-dot-warn' : 'lt-dot-idle'); + $trendCrit = ($stats['critical'] > 0) ? 'lt-dot-warn' : 'lt-dot-up'; + $trendUnassi = ($stats['unassigned'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle'; + $trendToday = ($stats['created_today'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle'; + $trendClosed = ($stats['closed_today'] > 0) ? 'lt-dot-up' : 'lt-dot-idle'; + ?> +
[ # ]
-
+
+ + +
Open Tickets
@@ -97,7 +111,10 @@ include __DIR__ . '/layout_header.php'; title="Click to filter critical (P1) tickets" aria-label="Critical P1 tickets">
[ ! ]
-
+
+ + +
Critical (P1)
@@ -107,7 +124,10 @@ include __DIR__ . '/layout_header.php'; title="Click to filter unassigned tickets" aria-label="Unassigned tickets">
[ @ ]
-
+
+ + +
Unassigned
@@ -116,7 +136,10 @@ include __DIR__ . '/layout_header.php'; title="Tickets created today" aria-label="Tickets created today">
[ + ]
-
+
+ + +
Created Today
@@ -126,7 +149,10 @@ include __DIR__ . '/layout_header.php'; title="Click to filter closed tickets" aria-label="Closed tickets today">
[ OK ]
-
+
+ + +
Closed Today
@@ -142,6 +168,54 @@ include __DIR__ . '/layout_header.php'; + + +
+ + Team Workload + — open tickets by assignee + +
+ +
+ 0 ? round(($count / $maxLoad) * 100) : 0; + $barClass = $pct >= 80 ? 'lt-progress--red' : ($pct >= 50 ? 'lt-progress--cyan' : 'lt-progress--green'); + $words = array_filter(explode(' ', $name)); + $initials = strtoupper(implode('', array_map(fn($w) => $w[0], array_slice($words, 0, 2)))); + $avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', '']; + $avatarColor = $avatarColors[abs(crc32($name)) % count($avatarColors)]; + $userId = (int)($a['user_id'] ?? 0); + ?> +
+ +
+
+
+
+
+
+ +
+ +
+
+
+ +