Compare commits

..

No commits in common. "80120ecc84e1552df236cffab2cdc91b64b9c4c8" and "7eb2751cd4d67a3457023597f7e6fa6f6eb6c9a2" have entirely different histories.

3 changed files with 23 additions and 78 deletions

View File

@ -92,10 +92,7 @@ items first. (Items grandfathered from before teams existed appear under "No Tea
"Uncategorized".) "Uncategorized".)
The grid shows the trailing 12 months; each cell is the **most recent entry in that month** 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). Project months without (color + value for KPIs, color for projects, tooltip with date and notes). Rows expand in place:
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 **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 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. month's notes/headwinds/tailwinds/highlights/blockers. Clicking anywhere else collapses the row.

View File

@ -5,17 +5,14 @@ namespace App\Support;
use App\Enums\KpiEvaluation; use App\Enums\KpiEvaluation;
use App\Models\Kpi; use App\Models\Kpi;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectEntry;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
/** /**
* Builds the rolling 12-month bowler grid. Rows are grouped * Builds the rolling 12-month bowler grid. Rows are grouped
* Team -> Category (by name, case-insensitively, so same-named teams * Team -> Category (by name, case-insensitively, so same-named teams
* from different owners merge on shared charts); within each month the * from different owners merge on shared charts); within each month the
* most recent entry wins (entry_date desc, id desc). Project months * most recent entry wins (entry_date desc, id desc). Items without a
* without their own entry carry the most recent earlier entry forward, * team/category fall into "No Team" / "Uncategorized", sorted last.
* so the latest status stays visible through the current month. Items
* without a team/category fall into "No Team" / "Uncategorized", sorted last.
*/ */
class BowlerGrid class BowlerGrid
{ {
@ -68,7 +65,7 @@ class BowlerGrid
'chart' => KpiChart::build($kpi, $cells, $months), 'chart' => KpiChart::build($kpi, $cells, $months),
]; ];
}) })
->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p, $months, $start)])); ->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p)]));
// Group by team then category, merging names case-insensitively. // Group by team then category, merging names case-insensitively.
$grouped = []; $grouped = [];
@ -126,60 +123,17 @@ class BowlerGrid
} }
/** /**
* @param array<int, array{key: string, label: string}> $months * @return array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string}>
* @param string $start first day of the 12-month window (Y-m-d)
* @return array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string, carriedForward: bool, details: array}>
*/ */
private static function projectCells(Project $project, array $months, string $start): array private static function projectCells(Project $project): array
{ {
$cells = []; $cells = [];
$byMonth = [];
// Entries are ordered ascending, so the last write per month wins.
foreach ($project->entries as $entry) { foreach ($project->entries as $entry) {
$key = $entry->entry_date->format('Y-m'); $cells[$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, 'status' => $entry->status,
'display' => strtoupper(substr($entry->status->value, 0, 1)), 'display' => strtoupper(substr($entry->status->value, 0, 1)),
'tooltip' => $carriedForward ? 'Carried forward from ' . $tooltip : $tooltip, 'tooltip' => trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? "{$entry->notes}" : '')),
'carriedForward' => $carriedForward,
// Payload for the click-to-expand detail row on charts // Payload for the click-to-expand detail row on charts
'details' => [ 'details' => [
'date' => $entry->entry_date->format('Y-m-d'), 'date' => $entry->entry_date->format('Y-m-d'),
@ -190,8 +144,10 @@ class BowlerGrid
'tailwinds' => $entry->tailwinds, 'tailwinds' => $entry->tailwinds,
'highlights' => $entry->highlights, 'highlights' => $entry->highlights,
'blockers' => $entry->blockers, 'blockers' => $entry->blockers,
'carriedForward' => $carriedForward,
], ],
]; ];
} }
return $cells;
}
} }

View File

@ -88,8 +88,7 @@
</table> </table>
</div> </div>
<p class="mt-2 text-xs text-gray-400"> <p class="mt-2 text-xs text-gray-400">
Each cell shows the most recent entry in that month; project months without an entry carry the most recent Each cell shows the most recent entry in that month. The current month is highlighted.
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. Click a KPI row to expand its trend chart; click a project row for its latest details, or a specific cell for that month.
</p> </p>
@ -147,13 +146,6 @@
function populate(td, d) { function populate(td, d) {
td.innerHTML = ''; 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'); var head = document.createElement('div');
head.className = 'mb-2 flex items-center gap-3'; head.className = 'mb-2 flex items-center gap-3';
var date = document.createElement('span'); var date = document.createElement('span');