Project Bring Forward on Bowler
This commit is contained in:
parent
7eb2751cd4
commit
8db7f91c55
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string}>
|
||||
* @param array<int, array{key: string, label: string}> $months
|
||||
* @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
|
||||
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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@
|
|||
</table>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
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.
|
||||
</p>
|
||||
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue