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:
+79
-5
@@ -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>▸</span> Team Workload
|
||||
<span class="lt-text-xs lt-text-muted" style="font-weight:normal">— 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
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
Reference in New Issue
Block a user