feat: trend dots on stat cards, team workload panel, stat model improvement

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:04:41 -04:00
parent fca4896e0d
commit 0d8edc9d34
3 changed files with 107 additions and 11 deletions
+79 -5
View File
@@ -82,12 +82,26 @@ include __DIR__ . '/layout_header.php';
<?php if (isset($stats)): ?>
<div class="lt-stats-grid" id="statsGrid">
<?php
// Trend indicators — derived from existing stats without extra DB query
// Logic: if more closed today than created → improving (green), if more created → warn, else idle
$trendOpen = ($stats['closed_today'] > $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';
?>
<div class="lt-stat-card stat-open" role="button" tabindex="0"
data-filter-key="status" data-filter-val="Open,Pending,In Progress"
title="Click to filter by active tickets" aria-label="Open tickets">
<div class="lt-stat-icon">[ # ]</div>
<div class="lt-stat-info">
<div class="lt-stat-value"><?= (int)$stats['open_tickets'] ?></div>
<div class="lt-stat-value">
<?= (int)$stats['open_tickets'] ?>
<span class="lt-dot <?= $trendOpen ?>" style="margin-left:0.4rem;vertical-align:middle" aria-hidden="true"></span>
</div>
<div class="lt-stat-label">Open Tickets</div>
</div>
</div>
@@ -97,7 +111,10 @@ include __DIR__ . '/layout_header.php';
title="Click to filter critical (P1) tickets" aria-label="Critical P1 tickets">
<div class="lt-stat-icon lt-text-danger">[ ! ]</div>
<div class="lt-stat-info">
<div class="lt-stat-value"><?= (int)$stats['critical'] ?></div>
<div class="lt-stat-value">
<?= (int)$stats['critical'] ?>
<span class="lt-dot <?= $trendCrit ?>" style="margin-left:0.4rem;vertical-align:middle" aria-hidden="true"></span>
</div>
<div class="lt-stat-label">Critical (P1)</div>
</div>
</div>
@@ -107,7 +124,10 @@ include __DIR__ . '/layout_header.php';
title="Click to filter unassigned tickets" aria-label="Unassigned tickets">
<div class="lt-stat-icon lt-text-amber">[ @ ]</div>
<div class="lt-stat-info">
<div class="lt-stat-value"><?= (int)$stats['unassigned'] ?></div>
<div class="lt-stat-value">
<?= (int)$stats['unassigned'] ?>
<span class="lt-dot <?= $trendUnassi ?>" style="margin-left:0.4rem;vertical-align:middle" aria-hidden="true"></span>
</div>
<div class="lt-stat-label">Unassigned</div>
</div>
</div>
@@ -116,7 +136,10 @@ include __DIR__ . '/layout_header.php';
title="Tickets created today" aria-label="Tickets created today">
<div class="lt-stat-icon lt-text-cyan">[ + ]</div>
<div class="lt-stat-info">
<div class="lt-stat-value"><?= (int)$stats['created_today'] ?></div>
<div class="lt-stat-value">
<?= (int)$stats['created_today'] ?>
<span class="lt-dot <?= $trendToday ?>" style="margin-left:0.4rem;vertical-align:middle" aria-hidden="true"></span>
</div>
<div class="lt-stat-label">Created Today</div>
</div>
</div>
@@ -126,7 +149,10 @@ include __DIR__ . '/layout_header.php';
title="Click to filter closed tickets" aria-label="Closed tickets today">
<div class="lt-stat-icon lt-text-muted">[ OK ]</div>
<div class="lt-stat-info">
<div class="lt-stat-value"><?= (int)$stats['closed_today'] ?></div>
<div class="lt-stat-value">
<?= (int)$stats['closed_today'] ?>
<span class="lt-dot <?= $trendClosed ?>" style="margin-left:0.4rem;vertical-align:middle" aria-hidden="true"></span>
</div>
<div class="lt-stat-label">Closed Today</div>
</div>
</div>
@@ -142,6 +168,54 @@ include __DIR__ . '/layout_header.php';
</div>
<?php endif ?>
<?php if (!empty($stats['by_assignee'])): ?>
<!-- ═══════════════════════════════════════════════════════════
TEAM WORKLOAD PANEL
═══════════════════════════════════════════════════════════ -->
<details class="lt-frame workload-panel" style="margin-bottom:0.75rem" id="workloadPanel">
<summary class="lt-section-header" style="cursor:pointer;list-style:none;display:flex;align-items:center;gap:0.5rem">
<span>&#x25B8;</span> Team Workload
<span class="lt-text-xs lt-text-muted" style="font-weight:normal">&mdash; open tickets by assignee</span>
</summary>
<div class="lt-section-body">
<?php
$byAssignee = $stats['by_assignee'];
$maxLoad = max(array_column($byAssignee, 'open_count') ?: [1]);
?>
<div class="workload-grid">
<?php foreach ($byAssignee as $a):
$count = (int)$a['open_count'];
$name = $a['display_name'] ?? $a['username'] ?? 'Unknown';
$pct = $maxLoad > 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);
?>
<div class="workload-item">
<div class="lt-avatar lt-avatar--sm <?= $avatarColor ?>" aria-hidden="true" title="<?= htmlspecialchars($name) ?>">
<?php if ($userId > 0): ?>
<img src="/api/user_avatar.php?user_id=<?= $userId ?>" alt="" class="lt-avatar-img" onerror="this.style.display='none'">
<?php endif ?>
<span class="lt-avatar-initials"><?= htmlspecialchars($initials) ?></span>
</div>
<div class="workload-info">
<div class="workload-name lt-text-xs"><?= htmlspecialchars($name) ?></div>
<div class="lt-progress lt-progress--sm <?= $barClass ?>" style="margin:0.2rem 0"
aria-label="<?= $count ?> open tickets" title="<?= $count ?> open">
<div class="lt-progress-bar" style="width:<?= max(4, $pct) ?>%"></div>
</div>
</div>
<span class="lt-text-xs lt-text-muted workload-count"><?= $count ?></span>
</div>
<?php endforeach ?>
</div>
</div>
</details>
<?php endif ?>
<!-- ═══════════════════════════════════════════════════════════
VIEW TABS (Table / Kanban) — dual-purpose: lt.tabs + dashboard.js set-view-mode
═══════════════════════════════════════════════════════════ -->