--- 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 `