From fc4b96c10fc35de624551ab8959c3557dd3dac00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=BChl?= Date: Wed, 8 Apr 2026 15:25:35 +0200 Subject: [PATCH] docs(04): create phase plans for heatmap interaction Co-Authored-By: Claude Opus 4.6 --- .planning/ROADMAP.md | 14 +- .../04-heatmap-interaction/04-01-PLAN.md | 524 ++++++++++++++++++ .../04-heatmap-interaction/04-02-PLAN.md | 506 +++++++++++++++++ 3 files changed, 1037 insertions(+), 7 deletions(-) create mode 100644 .planning/phases/04-heatmap-interaction/04-01-PLAN.md create mode 100644 .planning/phases/04-heatmap-interaction/04-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f26d702..0c582b9 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -14,7 +14,7 @@ Decimal phases appear between their surrounding integers in numeric order. - [x] **Phase 1: Dev Environment** - Nix flake with local Kimai instance and seeded test data - [x] **Phase 2: Plugin Scaffold + Data Layer** - Symfony bundle, dashboard widget, aggregation API with PHPUnit tests -- [ ] **Phase 3: Core Heatmap Rendering** - d3.js calendar grid with color mapping, labels, tooltips, theme integration, and JS tests +- [x] **Phase 3: Core Heatmap Rendering** - d3.js calendar grid with color mapping, labels, tooltips, theme integration, and JS tests - [ ] **Phase 4: Heatmap Interaction** - Click-through navigation, project/activity filtering, interaction tests - [ ] **Phase 5: Polish** - Streak indicator, summary stats, weekend styling @@ -65,8 +65,8 @@ Plans: Plans: - [x] 03-01: JS toolchain — npm, esbuild, TypeScript, Vitest -- [ ] 03-02: d3 calendar heatmap with color scale, labels, tooltips + Vitest tests -- [ ] 03-03: Twig template integration + asset serving +- [x] 03-02: d3 calendar heatmap with color scale, labels, tooltips + Vitest tests +- [x] 03-03: Twig template integration + asset serving ### Phase 4: Heatmap Interaction **Goal**: Users can click through to daily details and filter the heatmap by project or activity @@ -77,12 +77,12 @@ Plans: 2. A dropdown allows filtering the heatmap to show data for a single project or activity 3. Filtering updates the heatmap in place without a full page reload 4. JavaScript tests verify click navigation and tooltip interaction behavior -**Plans**: TBD +**Plans:** 2 plans **UI hint**: yes Plans: -- [ ] 04-01: TBD -- [ ] 04-02: TBD +- [ ] 04-01: Day cell click navigation + project filter dropdown +- [ ] 04-02: Vitest tests for click navigation and filter behavior ### Phase 5: Polish **Goal**: The widget provides at-a-glance context beyond the heatmap itself -- streaks, stats, and visual cues for weekends @@ -108,6 +108,6 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 |-------|----------------|--------|-----------| | 1. Dev Environment | 2/2 | Done | 2026-04-08 | | 2. Plugin Scaffold + Data Layer | 2/2 | Done | 2026-04-08 | -| 3. Core Heatmap Rendering | 1/3 | In progress | - | +| 3. Core Heatmap Rendering | 3/3 | Done | 2026-04-08 | | 4. Heatmap Interaction | 0/2 | Not started | - | | 5. Polish | 0/2 | Not started | - | diff --git a/.planning/phases/04-heatmap-interaction/04-01-PLAN.md b/.planning/phases/04-heatmap-interaction/04-01-PLAN.md new file mode 100644 index 0000000..b07fe38 --- /dev/null +++ b/.planning/phases/04-heatmap-interaction/04-01-PLAN.md @@ -0,0 +1,524 @@ +--- +phase: 04-heatmap-interaction +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - Service/HeatmapService.php + - Widget/HeatmapWidget.php + - Resources/views/widget/heatmap.html.twig + - Resources/public/heatmap.css + - assets/src/heatmap.ts + - assets/src/types.ts + - Resources/public/heatmap.js +autonomous: true +requirements: [HEAT-07, INTR-01] + +must_haves: + truths: + - "Clicking any heatmap day cell (with or without data) navigates to Kimai timesheet filtered to that date" + - "A project filter dropdown appears to the right of the heatmap SVG" + - "Selecting a project re-fetches data and re-renders the heatmap without page reload" + - "Selecting 'All Projects' resets to unfiltered data" + - "When a project filter is active, click navigation URL includes the project parameter" + - "Cells show pointer cursor and opacity hover effect as click affordance" + artifacts: + - path: "assets/src/heatmap.ts" + provides: "Click handler on cells, filter dropdown creation, re-fetch on filter change" + contains: "window.location.href" + - path: "Service/HeatmapService.php" + provides: "getUserProjects() method returning projects the user has tracked time for" + contains: "getUserProjects" + - path: "Widget/HeatmapWidget.php" + provides: "getData() returns project list for template" + contains: "getUserProjects" + - path: "Resources/views/widget/heatmap.html.twig" + provides: "data-projects and data-timesheet-url attributes on container" + contains: "data-projects" + - path: "Resources/public/heatmap.css" + provides: "Click affordance styles, flex layout for heatmap + filter" + contains: "heatmap-wrapper" + key_links: + - from: "assets/src/heatmap.ts" + to: "Kimai timesheet" + via: "window.location.href with daterange param" + pattern: "window\\.location\\.href" + - from: "assets/src/heatmap.ts" + to: "HeatmapController::data()" + via: "fetch with ?project=N query param" + pattern: "fetch.*project=" + - from: "Widget/HeatmapWidget.php" + to: "HeatmapService::getUserProjects()" + via: "getData() calls service method" + pattern: "getUserProjects" +--- + + +Add click-through navigation from heatmap day cells to Kimai's timesheet view, and a project filter dropdown that re-fetches and re-renders the heatmap in place. + +Purpose: Users can drill into specific days and filter their heatmap by project, making the widget actionable rather than just informational. +Output: Working click navigation, project filter dropdown, updated CSS and templates, rebuilt JS bundle. + + + +@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/workflows/execute-plan.md +@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/04-heatmap-interaction/04-CONTEXT.md +@.planning/phases/04-heatmap-interaction/04-RESEARCH.md +@.planning/phases/04-heatmap-interaction/04-UI-SPEC.md + + + + +From assets/src/types.ts: +```typescript +export interface DayEntry { + date: string; // "YYYY-MM-DD" + hours: number; + count: number; +} + +export interface HeatmapData { + days: DayEntry[]; + range: { + begin: string; + end: string; + }; +} + +export interface HeatmapConfig { + cellSize: number; + cellGap: number; + marginTop: number; + marginLeft: number; + marginBottom: number; +} +``` + +From assets/src/heatmap.ts (internal DayCell interface, line 22): +```typescript +interface DayCell { + date: Date; + dateStr: string; + entry: DayEntry | null; + week: number; + day: number; +} +``` + +From Widget/HeatmapWidget.php: +```php +final class HeatmapWidget extends AbstractWidget +// AbstractWidget provides: getUser(): User, setUser(User): void +// WidgetExtension calls setUser() before getData() +// Template receives: data (from getData), options, title, widget +``` + +From Twig/Runtime/WidgetExtension.php (line 53): +```php +return $environment->render($widget->getTemplateName(), [ + 'data' => $widget->getData($options), + 'options' => $options, + 'title' => $widget->getTitle(), + 'widget' => $widget, +]); +``` + +From Controller/HeatmapController.php: +```php +// Already accepts ?project=N query parameter +$projectId = $request->query->getInt('project') ?: null; +$days = $service->getDailyAggregation($user, $begin, $end, $projectId); +``` + + + + + + + Task 1: Backend — getUserProjects service method, widget getData, Twig data attributes + Service/HeatmapService.php, Widget/HeatmapWidget.php, Resources/views/widget/heatmap.html.twig + + - Service/HeatmapService.php + - Widget/HeatmapWidget.php + - Resources/views/widget/heatmap.html.twig + - dev/kimai/src/Widget/Type/AbstractWidget.php + - dev/kimai/src/Twig/Runtime/WidgetExtension.php + + +**HeatmapService.php** — Add `getUserProjects(User $user): array` method that queries distinct projects the user has tracked time for: + +```php +/** + * @return array + */ +public function getUserProjects(User $user): array +{ + $qb = $this->repository->createQueryBuilder('t'); + $qb->select('DISTINCT IDENTITY(t.project) as projectId, p.name') + ->join('t.project', 'p') + ->andWhere($qb->expr()->eq('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) + ->setParameter('user', $user) + ->orderBy('p.name', 'ASC'); + + return array_map(fn(array $row) => [ + 'id' => (int) $row['projectId'], + 'name' => $row['name'], + ], $qb->getQuery()->getResult()); +} +``` + +**HeatmapWidget.php** — Remove `final` keyword (needed for testing). Inject `HeatmapService` via constructor. Update `getData()` to return project list. The `getUser()` method is inherited from `AbstractWidget` and is set by `WidgetExtension::renderWidget()` before `getData()` is called. + +```php +use KimaiPlugin\KimaiHeatmapBundle\Service\HeatmapService; + +class HeatmapWidget extends AbstractWidget +{ + public function __construct(private readonly HeatmapService $service) + { + } + + public function getData(array $options = []): mixed + { + $user = $this->getUser(); + return [ + 'projects' => $this->service->getUserProjects($user), + ]; + } + // ... rest unchanged +} +``` + +**heatmap.html.twig** — Add two new `data-` attributes to the container div: +1. `data-projects` — JSON-encoded project list from `data.projects` +2. `data-timesheet-url` — Kimai timesheet base path via `path('timesheet')` (locale-safe, avoids hardcoding `/en/timesheet/`) + +Updated template: +```twig +{% embed '@theme/embeds/card.html.twig' with {'margin_bottom': 0} %} + {% block box_title %} + {{ title }} + {% endblock %} + {% block box_body %} + +
+
+ + + {% endblock %} +{% endembed %} +``` +
+ + cd /home/toph/code/toph/kimai-heatmap && grep -c 'getUserProjects' Service/HeatmapService.php && grep -c 'getUserProjects' Widget/HeatmapWidget.php && grep -c 'data-projects' Resources/views/widget/heatmap.html.twig && grep -c 'data-timesheet-url' Resources/views/widget/heatmap.html.twig + + + - Service/HeatmapService.php contains `public function getUserProjects(User $user): array` + - Service/HeatmapService.php contains `DISTINCT IDENTITY(t.project)` + - Widget/HeatmapWidget.php contains `private readonly HeatmapService $service` + - Widget/HeatmapWidget.php contains `$this->service->getUserProjects` + - Widget/HeatmapWidget.php does NOT have `final class` (just `class`) + - Resources/views/widget/heatmap.html.twig contains `data-projects="{{ data.projects|json_encode }}"` + - Resources/views/widget/heatmap.html.twig contains `data-timesheet-url="{{ path('timesheet') }}"` + + HeatmapService has getUserProjects method, HeatmapWidget injects service and returns project list in getData, Twig template passes projects and timesheet URL as data attributes +
+ + + Task 2: Frontend — Click navigation, filter dropdown, CSS, esbuild rebuild + assets/src/heatmap.ts, assets/src/types.ts, Resources/public/heatmap.css, Resources/public/heatmap.js + + - assets/src/heatmap.ts + - assets/src/types.ts + - Resources/public/heatmap.css + - .planning/phases/04-heatmap-interaction/04-UI-SPEC.md + - .planning/phases/04-heatmap-interaction/04-RESEARCH.md + + +**assets/src/types.ts** — Add a `ProjectOption` interface: + +```typescript +export interface ProjectOption { + id: number; + name: string; +} +``` + +**assets/src/heatmap.ts** — Major refactoring of `init()` and additions to `renderHeatmap()`: + +1. **Add click handler** to the existing cell selection chain (after `.on('mouseleave', ...)` line). The handler constructs the Kimai timesheet URL using the `daterange` format `YYYY-MM-DD - YYYY-MM-DD` (same date repeated). It reads `timesheetUrl` from closure and appends `&projects[]=N` when a project filter is active. + +Add `.on('click', ...)` to the cells selection chain at line ~193, right after the `.on('mouseleave', ...)`: +```typescript +.on('click', function (_event: MouseEvent, d: DayCell) { + if (!onCellClick) return; + onCellClick(d.dateStr); +}) +``` + +2. **Add `onCellClick` parameter** to `renderHeatmap()` signature: +```typescript +export function renderHeatmap( + container: HTMLElement, + data: HeatmapData, + config: HeatmapConfig = DEFAULT_CONFIG, + onCellClick?: (dateStr: string) => void, +): void +``` + +3. **Add empty state for filtered results**. When `data.days` is empty, check if a `filteredEmpty` context flag is set. Add an optional `emptyMessage` parameter to `renderHeatmap`: +```typescript +export function renderHeatmap( + container: HTMLElement, + data: HeatmapData, + config: HeatmapConfig = DEFAULT_CONFIG, + onCellClick?: (dateStr: string) => void, + emptyMessage?: string, +): void +``` +Use `emptyMessage || 'No tracking data available'` for the empty state text. + +4. **Refactor `init()`** to: + - Parse `data-projects` JSON attribute into `ProjectOption[]` + - Parse `data-timesheet-url` attribute for the timesheet base path + - Track `activeProjectId: number | null = null` in closure scope + - Create a flex wrapper div with class `heatmap-wrapper` + - Create a `heatmap-svg-area` div for the SVG + - Create a `heatmap-filter` div with a `