From 8db7f91c555e818e3bf42172ef716f7b5a116a74 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Wed, 2 Sep 2026 16:13:58 -0600 Subject: [PATCH] Project Bring Forward on Bowler --- README.md | 5 +- src/app/Support/BowlerGrid.php | 86 ++++++++++++++++------ src/resources/views/charts/_grid.blade.php | 10 ++- 3 files changed, 78 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index baa7396..07c7d87 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,10 @@ items first. (Items grandfathered from before teams existed appear under "No Tea "Uncategorized".) The grid shows the trailing 12 months; each cell is the **most recent entry in that month** -(color + value for KPIs, color for projects, tooltip with date and notes). Rows expand in place: +(color + value for KPIs, color for projects, tooltip with date and notes). Project months without +their own entry **carry the most recent earlier entry forward** (marked "Carried forward from …" in the +tooltip and detail row), so a project's latest status stays visible through the current month — +months before its first entry remain blank. Rows expand in place: **click a KPI row** for an inline SVG trend chart with its threshold zones shaded (line + status-colored points for numeric KPIs, a met/missed timeline for milestones); **click a project's month cell** for that month's notes/headwinds/tailwinds/highlights/blockers. Clicking anywhere else collapses the row. diff --git a/src/app/Support/BowlerGrid.php b/src/app/Support/BowlerGrid.php index 487c3af..201f5e2 100644 --- a/src/app/Support/BowlerGrid.php +++ b/src/app/Support/BowlerGrid.php @@ -5,14 +5,17 @@ namespace App\Support; use App\Enums\KpiEvaluation; use App\Models\Kpi; use App\Models\Project; +use App\Models\ProjectEntry; use Illuminate\Support\Collection; /** * Builds the rolling 12-month bowler grid. Rows are grouped * Team -> Category (by name, case-insensitively, so same-named teams * from different owners merge on shared charts); within each month the - * most recent entry wins (entry_date desc, id desc). Items without a - * team/category fall into "No Team" / "Uncategorized", sorted last. + * most recent entry wins (entry_date desc, id desc). Project months + * without their own entry carry the most recent earlier entry forward, + * so the latest status stays visible through the current month. Items + * without a team/category fall into "No Team" / "Uncategorized", sorted last. */ class BowlerGrid { @@ -65,7 +68,7 @@ class BowlerGrid 'chart' => KpiChart::build($kpi, $cells, $months), ]; }) - ->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p)])); + ->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p, $months, $start)])); // Group by team then category, merging names case-insensitively. $grouped = []; @@ -123,31 +126,72 @@ class BowlerGrid } /** - * @return array + * @param array $months + * @param string $start first day of the 12-month window (Y-m-d) + * @return array */ - private static function projectCells(Project $project): array + private static function projectCells(Project $project, array $months, string $start): array { $cells = []; + $byMonth = []; + // Entries are ordered ascending, so the last write per month wins. foreach ($project->entries as $entry) { - $cells[$entry->entry_date->format('Y-m')] = [ - 'status' => $entry->status, - 'display' => strtoupper(substr($entry->status->value, 0, 1)), - 'tooltip' => trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? " — {$entry->notes}" : '')), - // Payload for the click-to-expand detail row on charts - 'details' => [ - 'date' => $entry->entry_date->format('Y-m-d'), - 'statusLabel' => $entry->status->label(), - 'statusClasses' => $entry->status->badgeClasses(), - 'notes' => $entry->notes, - 'headwinds' => $entry->headwinds, - 'tailwinds' => $entry->tailwinds, - 'highlights' => $entry->highlights, - 'blockers' => $entry->blockers, - ], - ]; + $key = $entry->entry_date->format('Y-m'); + $cells[$key] = self::entryCell($entry); + $byMonth[$key] = $entry; + } + + // Carry the most recent entry forward into any month without its + // own entry (including the current month). Months before the first + // entry ever stay blank. The fill is seeded with the latest entry + // that predates the window, so an old "dormant" status also carries + // across the gap up to the first recent entry. + $filler = $project->entries() + ->where('entry_date', '<', $start) + ->orderByDesc('entry_date') + ->orderByDesc('id') + ->first(); + + foreach ($months as $month) { + $key = $month['key']; + if (isset($byMonth[$key])) { + $filler = $byMonth[$key]; + continue; + } + + if ($filler !== null) { + $cells[$key] = self::entryCell($filler, carriedForward: true); + } } return $cells; } + + /** + * @return array{status: \App\Enums\RagStatus, display: string, tooltip: string, carriedForward: bool, details: array} + */ + private static function entryCell(ProjectEntry $entry, bool $carriedForward = false): array + { + $tooltip = trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? " — {$entry->notes}" : '')); + + return [ + 'status' => $entry->status, + 'display' => strtoupper(substr($entry->status->value, 0, 1)), + 'tooltip' => $carriedForward ? 'Carried forward from ' . $tooltip : $tooltip, + 'carriedForward' => $carriedForward, + // Payload for the click-to-expand detail row on charts + 'details' => [ + 'date' => $entry->entry_date->format('Y-m-d'), + 'statusLabel' => $entry->status->label(), + 'statusClasses' => $entry->status->badgeClasses(), + 'notes' => $entry->notes, + 'headwinds' => $entry->headwinds, + 'tailwinds' => $entry->tailwinds, + 'highlights' => $entry->highlights, + 'blockers' => $entry->blockers, + 'carriedForward' => $carriedForward, + ], + ]; + } } diff --git a/src/resources/views/charts/_grid.blade.php b/src/resources/views/charts/_grid.blade.php index 809b365..8c4a55d 100644 --- a/src/resources/views/charts/_grid.blade.php +++ b/src/resources/views/charts/_grid.blade.php @@ -88,7 +88,8 @@

- Each cell shows the most recent entry in that month. The current month is highlighted. + Each cell shows the most recent entry in that month; project months without an entry carry the most recent + status forward (the tooltip marks it). The current month is highlighted. Click a KPI row to expand its trend chart; click a project row for its latest details, or a specific cell for that month.

@@ -146,6 +147,13 @@ function populate(td, d) { td.innerHTML = ''; + if (d.carriedForward) { + var cf = document.createElement('p'); + cf.className = 'mb-2 text-xs text-gray-500'; + cf.textContent = 'No entry recorded for this month — showing the most recent entry.'; + td.appendChild(cf); + } + var head = document.createElement('div'); head.className = 'mb-2 flex items-center gap-3'; var date = document.createElement('span');