docs(phase-4): research heatmap interaction domain
This commit is contained in:
parent
63f483a173
commit
4d0b8d5b5d
1 changed files with 417 additions and 0 deletions
417
.planning/phases/04-heatmap-interaction/04-RESEARCH.md
Normal file
417
.planning/phases/04-heatmap-interaction/04-RESEARCH.md
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
# Phase 4: Heatmap Interaction - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-04-08
|
||||||
|
**Domain:** d3.js click interaction, Kimai timesheet URL routing, Symfony data passing to Twig/JS
|
||||||
|
**Confidence:** HIGH
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 4 adds 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. The codebase is well-prepared: `renderHeatmap()` already accepts data and re-renders from scratch (clear + redraw pattern), and the `HeatmapController::data()` endpoint already accepts `?project=N`.
|
||||||
|
|
||||||
|
The main technical findings are: (1) Kimai's timesheet daterange URL format is locale-dependent and uses a `begin - end` range, but Kimai always accepts the fallback `yyyy-MM-dd` format, so we can safely use `YYYY-MM-DD - YYYY-MM-DD` with the same date for both begin and end to filter to a single day; (2) the project list should be passed as a `data-projects` JSON attribute on the container div, populated by the widget/Twig template using `ProjectRepository`; (3) existing test infrastructure (Vitest + jsdom) is sufficient for all new interaction tests.
|
||||||
|
|
||||||
|
**Primary recommendation:** Add click handlers to d3 cells during rendering, build the filter dropdown as a plain `<select>` with Tabler classes, and test everything with Vitest by mocking `window.location` and `fetch`.
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- Clicking any day cell navigates to Kimai's timesheet list filtered by that date (`/en/timesheet/?daterange=YYYY-MM-DD`)
|
||||||
|
- Empty cells (no data) are clickable too -- user may want to add time for that day
|
||||||
|
- Pointer cursor + subtle hover highlight (opacity change) on all cells as click affordance
|
||||||
|
- If a project filter is active, preserve it in the timesheet URL (`&projects[]=N`)
|
||||||
|
- Filter dropdown positioned next to the heatmap SVG (right side), using the spare horizontal space
|
||||||
|
- Projects only -- no activity filter (API already supports `?project=N`)
|
||||||
|
- Default state: "All Projects" selected, no filter applied
|
||||||
|
- Selecting a project fetches from API with `?project=N`, re-renders heatmap in place; color scale recalculates for filtered data
|
||||||
|
- Vitest tests for click navigation: verify click handler sets `window.location.href` to correct Kimai URL pattern
|
||||||
|
- Vitest tests for filter dropdown: verify dropdown renders, selecting fires fetch with correct query param, heatmap re-renders with new data
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Exact Kimai timesheet URL format (verify against Kimai source)
|
||||||
|
- Filter dropdown styling details (should use Tabler/Kimai form classes)
|
||||||
|
- How to populate the project list (new API endpoint or extend existing one)
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
None -- discussion stayed within phase scope.
|
||||||
|
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
<phase_requirements>
|
||||||
|
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|------------------|
|
||||||
|
| HEAT-07 | Clicking a day cell navigates to Kimai timesheet view filtered to that date | Verified Kimai timesheet URL pattern: `/en/timesheet/?daterange={date} - {date}` with fallback `yyyy-MM-dd` format always accepted |
|
||||||
|
| INTR-01 | Dropdown filter to view heatmap for a specific project or activity | API endpoint already supports `?project=N`; project list via `data-projects` JSON attribute from Twig template |
|
||||||
|
| TEST-04 | JavaScript tests for tooltip and click interaction behavior | Existing Vitest + jsdom setup; mock `window.location.href` and `global.fetch` |
|
||||||
|
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
No new packages needed. All work uses existing dependencies.
|
||||||
|
|
||||||
|
### Core (already installed)
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| d3-selection | ^3.0.0 | Click event handlers on SVG rects | Already used for mouseenter/mouseleave; `.on('click', ...)` is the same API |
|
||||||
|
| vitest | ^4.1.3 | Interaction tests | Already configured with jsdom environment |
|
||||||
|
|
||||||
|
### Supporting (already installed)
|
||||||
|
| Library | Version | Purpose | When to Use |
|
||||||
|
|---------|---------|---------|-------------|
|
||||||
|
| esbuild | ^0.28.0 | Rebuild bundled JS after adding interaction code | After any TypeScript changes |
|
||||||
|
|
||||||
|
No `npm install` needed for this phase. [VERIFIED: package.json in project root]
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Kimai Timesheet URL Format (CRITICAL)
|
||||||
|
|
||||||
|
Kimai's daterange parameter is **locale-dependent** but always accepts the fallback format `yyyy-MM-dd`. The format is a **range** (`begin - end`), not a single date. To filter to one day, use the same date for both begin and end. [VERIFIED: `dev/kimai/src/Form/Type/DateRangeType.php` lines 200-201]
|
||||||
|
|
||||||
|
```
|
||||||
|
# Single day navigation:
|
||||||
|
/en/timesheet/?daterange=2026-04-08 - 2026-04-08
|
||||||
|
|
||||||
|
# With project filter:
|
||||||
|
/en/timesheet/?daterange=2026-04-08 - 2026-04-08&projects[]=5
|
||||||
|
```
|
||||||
|
|
||||||
|
The `DATE_SPACER` constant is ` - ` (space-dash-space). [VERIFIED: `DateRangeType::DATE_SPACER`]
|
||||||
|
|
||||||
|
**Note:** The CONTEXT.md says `/en/timesheet/?daterange=YYYY-MM-DD` but Kimai actually requires a range format. Use `YYYY-MM-DD - YYYY-MM-DD` with the same date repeated. The `/en/` locale prefix is correct for the default English locale. [VERIFIED: route name `timesheet` at path `/timesheet/`]
|
||||||
|
|
||||||
|
### Click Handler Pattern
|
||||||
|
|
||||||
|
Add `.on('click', ...)` to the existing d3 cell selection chain in `renderHeatmap()`. The click handler needs access to:
|
||||||
|
- `d.dateStr` -- the `YYYY-MM-DD` date string (already in `DayCell` interface)
|
||||||
|
- The base URL for the Kimai instance (derived from `window.location.origin`)
|
||||||
|
- The currently selected project filter ID (if any)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: existing heatmap.ts pattern + d3-selection API
|
||||||
|
.on('click', function (_event: MouseEvent, d: DayCell) {
|
||||||
|
const date = d.dateStr;
|
||||||
|
const daterange = `${date} - ${date}`;
|
||||||
|
let url = `/en/timesheet/?daterange=${encodeURIComponent(daterange)}`;
|
||||||
|
if (activeProjectId) {
|
||||||
|
url += `&projects[]=${activeProjectId}`;
|
||||||
|
}
|
||||||
|
window.location.href = url;
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter Dropdown Pattern
|
||||||
|
|
||||||
|
The filter dropdown is a native `<select>` element, not an SVG element. It sits outside the SVG in the DOM, styled with Tabler CSS classes. The project list comes from a `data-projects` attribute on the container div.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Parse project list from container attribute
|
||||||
|
const projectsJson = container.getAttribute('data-projects');
|
||||||
|
const projects: Array<{id: number; name: string}> = projectsJson ? JSON.parse(projectsJson) : [];
|
||||||
|
|
||||||
|
// Create <select> element
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.className = 'form-select form-select-sm';
|
||||||
|
select.setAttribute('aria-label', 'Filter by project');
|
||||||
|
|
||||||
|
// Default option
|
||||||
|
const defaultOpt = document.createElement('option');
|
||||||
|
defaultOpt.value = '';
|
||||||
|
defaultOpt.textContent = 'All Projects';
|
||||||
|
select.appendChild(defaultOpt);
|
||||||
|
|
||||||
|
// Project options
|
||||||
|
for (const p of projects) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = String(p.id);
|
||||||
|
opt.textContent = p.name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout Change
|
||||||
|
|
||||||
|
The current template has a single `#heatmap-container` div. This needs to become a flex container wrapping the SVG area and the filter. Two approaches:
|
||||||
|
|
||||||
|
**Option A (recommended):** Modify `renderHeatmap` to create the flex wrapper and filter internally. This keeps all DOM manipulation in one place and avoids requiring Twig template changes for the layout.
|
||||||
|
|
||||||
|
**Option B:** Add the wrapper structure in the Twig template. This couples template and JS more tightly.
|
||||||
|
|
||||||
|
Recommendation: Option A -- the JS already owns the container's innerHTML (it clears it on render). Add a wrapper div structure inside the JS.
|
||||||
|
|
||||||
|
### Twig Template Changes
|
||||||
|
|
||||||
|
The Twig template needs a new `data-projects` attribute. This requires the widget to pass project data to the template.
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{# heatmap.html.twig #}
|
||||||
|
<div id="heatmap-container"
|
||||||
|
data-url="{{ path('heatmap_data') }}"
|
||||||
|
data-projects="{{ projects|json_encode }}"
|
||||||
|
style="min-height: 150px; overflow-x: auto;">
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `projects` variable must be provided by the widget or a Twig extension.
|
||||||
|
|
||||||
|
### Project List Data Source
|
||||||
|
|
||||||
|
**Recommended approach:** Add a method to `HeatmapService` that queries the user's projects from the timesheet table (projects they've actually tracked time for), rather than all visible projects. This gives a more relevant dropdown. [ASSUMED]
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In HeatmapService
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The widget needs to pass this data to the template. Since `HeatmapWidget::getData()` currently returns `null`, it can be updated to return the project list. Alternatively, inject data through Twig globals or a controller. [ASSUMED]
|
||||||
|
|
||||||
|
**Simplest path:** Have the `HeatmapWidget` inject `HeatmapService`, call `getUserProjects()` in `getData()`, and pass it through to the template via widget options/data. The widget's `getTemplateName()` template receives `widget.data` which can hold the project list.
|
||||||
|
|
||||||
|
### Refactoring `init()` and `renderHeatmap()`
|
||||||
|
|
||||||
|
The current `init()` function fetches data and calls `renderHeatmap()`. For filter support, `init()` needs to:
|
||||||
|
1. Parse `data-projects` and create the filter dropdown
|
||||||
|
2. Store the base URL for re-fetching
|
||||||
|
3. On filter change, re-fetch with `?project=N` and re-render
|
||||||
|
|
||||||
|
**Recommended refactor:**
|
||||||
|
- `init()` creates the wrapper layout, builds the filter, fetches initial data, and calls `renderHeatmap()`
|
||||||
|
- Filter `change` handler re-fetches and re-calls `renderHeatmap()` on the SVG container area
|
||||||
|
- Click handler is added inside `renderHeatmap()` since it needs access to cell data
|
||||||
|
- A module-level or closure variable tracks the active project filter ID for URL generation
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
- **Embedding locale-specific date formats in JS:** Use `YYYY-MM-DD - YYYY-MM-DD` only. Kimai's DateRangeType always accepts this as a fallback. [VERIFIED: fallback format in DateRangeType.php]
|
||||||
|
- **Creating a separate API endpoint for project list:** Unnecessary network request. Pass via `data-projects` attribute at page load time.
|
||||||
|
- **Using `d3.select` for the dropdown:** The filter is a standard HTML element, not SVG. Use plain DOM API.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| Date formatting for URLs | Custom date formatter | `d3-time-format`'s `timeFormat('%Y-%m-%d')` already defined as `DATE_FORMAT` | Already in the codebase |
|
||||||
|
| Dropdown styling | Custom CSS for select elements | Tabler's `form-select form-select-sm` classes | Matches Kimai's existing UI |
|
||||||
|
| URL encoding | Manual string escaping | `encodeURIComponent()` for daterange param | The ` - ` separator needs encoding |
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Daterange Format Mismatch
|
||||||
|
**What goes wrong:** Using a single date in the `daterange` parameter instead of a range, causing Kimai to show no results or the current month
|
||||||
|
**Why it happens:** The CONTEXT.md suggests `?daterange=YYYY-MM-DD` but Kimai expects `begin - end` format
|
||||||
|
**How to avoid:** Always construct the URL as `?daterange=YYYY-MM-DD - YYYY-MM-DD` with the same date repeated for single-day filtering
|
||||||
|
**Warning signs:** Clicking a cell shows the wrong date range in the timesheet view
|
||||||
|
|
||||||
|
### Pitfall 2: Click Handler Overriding Tooltip
|
||||||
|
**What goes wrong:** Adding a click handler removes or conflicts with existing mouseenter/mouseleave tooltip handlers
|
||||||
|
**Why it happens:** If the click handler is added in a separate `.selectAll().on('click')` call instead of chained onto the original selection
|
||||||
|
**How to avoid:** Add `.on('click', ...)` in the same selection chain as the existing mouseenter/mouseleave handlers in `renderHeatmap()`
|
||||||
|
**Warning signs:** Tooltips stop working after adding click support
|
||||||
|
|
||||||
|
### Pitfall 3: SVG Hover CSS on `<rect>` Elements
|
||||||
|
**What goes wrong:** CSS `:hover` pseudo-class on SVG `<rect>` elements doesn't work consistently across browsers
|
||||||
|
**Why it happens:** SVG elements have different CSS interaction models. Some browsers need `pointer-events: fill` or `pointer-events: visible` on the element.
|
||||||
|
**How to avoid:** Set `pointer-events: fill` on `.heatmap-cell` rects. The UI spec's `cursor: pointer` and `opacity` transitions should work with this.
|
||||||
|
**Warning signs:** Hover effect doesn't appear on some cells or in some browsers
|
||||||
|
|
||||||
|
### Pitfall 4: Re-render Without Clearing Filter State
|
||||||
|
**What goes wrong:** After filter re-render, click handlers on new cells don't know about the active filter
|
||||||
|
**Why it happens:** If the active project ID is stored on cells rather than in closure/module scope
|
||||||
|
**How to avoid:** Store active filter selection in a closure variable accessible to the click handler factory, not on individual cell data
|
||||||
|
**Warning signs:** Clicking cells after filtering navigates without the project filter in the URL
|
||||||
|
|
||||||
|
### Pitfall 5: Empty Filtered Results
|
||||||
|
**What goes wrong:** Filtering to a project with no time entries shows the generic "No tracking data available" message
|
||||||
|
**Why it happens:** The empty state check in `renderHeatmap()` treats all empty results the same
|
||||||
|
**How to avoid:** Pass context to `renderHeatmap()` or check externally -- when filtering returns empty data, show "No tracking data for this project" instead
|
||||||
|
**Warning signs:** User filters to a project and sees a message that suggests no data exists at all
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Click Handler Integration (in renderHeatmap)
|
||||||
|
```typescript
|
||||||
|
// Source: d3-selection API + existing mouseenter/mouseleave pattern in heatmap.ts
|
||||||
|
// Add after .on('mouseleave', ...) in the cells selection chain
|
||||||
|
|
||||||
|
.on('click', function (_event: MouseEvent, d: DayCell) {
|
||||||
|
const daterange = `${d.dateStr} - ${d.dateStr}`;
|
||||||
|
let url = `/en/timesheet/?daterange=${encodeURIComponent(daterange)}`;
|
||||||
|
if (activeProjectId) {
|
||||||
|
url += `&projects[]=${activeProjectId}`;
|
||||||
|
}
|
||||||
|
window.location.href = url;
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter Change Handler
|
||||||
|
```typescript
|
||||||
|
// Source: standard DOM API + existing fetch pattern in init()
|
||||||
|
select.addEventListener('change', () => {
|
||||||
|
const projectId = select.value;
|
||||||
|
activeProjectId = projectId ? parseInt(projectId, 10) : null;
|
||||||
|
const fetchUrl = projectId ? `${baseUrl}?project=${projectId}` : baseUrl;
|
||||||
|
|
||||||
|
fetch(fetchUrl)
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json() as Promise<HeatmapData>;
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
renderHeatmap(svgContainer, data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('KimaiHeatmap: failed to load filtered data', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vitest Click Navigation Test
|
||||||
|
```typescript
|
||||||
|
// Source: Vitest docs + jsdom window.location mocking pattern
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
describe('click navigation', () => {
|
||||||
|
it('navigates to timesheet view for clicked day', () => {
|
||||||
|
// Mock window.location.href
|
||||||
|
const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({
|
||||||
|
...window.location,
|
||||||
|
href: '',
|
||||||
|
} as Location);
|
||||||
|
|
||||||
|
// Or use Object.defineProperty for href assignment
|
||||||
|
let navigatedUrl = '';
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: { href: '' },
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHeatmap(container, mockData);
|
||||||
|
const cell = container.querySelector('rect.heatmap-cell:not(.heatmap-empty)');
|
||||||
|
cell!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
|
||||||
|
expect(window.location.href).toContain('/en/timesheet/');
|
||||||
|
expect(window.location.href).toContain('daterange=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget getData() for Project List
|
||||||
|
```php
|
||||||
|
// Source: Kimai AbstractWidget pattern + TimesheetRepository DQL
|
||||||
|
// In Widget/HeatmapWidget.php
|
||||||
|
|
||||||
|
public function __construct(private readonly HeatmapService $service)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData(array $options = []): mixed
|
||||||
|
{
|
||||||
|
$user = $options['user'] ?? null;
|
||||||
|
if ($user === null) {
|
||||||
|
return ['projects' => []];
|
||||||
|
}
|
||||||
|
return ['projects' => $this->service->getUserProjects($user)];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | When Changed | Impact |
|
||||||
|
|--------------|------------------|--------------|--------|
|
||||||
|
| Kimai's form-based daterange | URL query string with fallback `yyyy-MM-dd` format | Stable (Kimai 2.x) | Always use fallback format in programmatic URLs |
|
||||||
|
|
||||||
|
## Assumptions Log
|
||||||
|
|
||||||
|
| # | Claim | Section | Risk if Wrong |
|
||||||
|
|---|-------|---------|---------------|
|
||||||
|
| A1 | Querying projects from timesheet table (projects user has tracked) is better UX than all visible projects | Architecture Patterns / Project List Data Source | Low -- fallback is to use ProjectRepository for all visible projects |
|
||||||
|
| A2 | `HeatmapWidget::getData()` receives `$options['user']` with the current user | Code Examples / Widget getData | Medium -- may need to get user from security token instead |
|
||||||
|
| A3 | The Twig template can access widget data via `widget.data` or similar accessor | Architecture Patterns / Twig Template Changes | Medium -- need to verify how AbstractWidget passes data to templates |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **How does AbstractWidget pass data to its Twig template?**
|
||||||
|
- What we know: `getData()` returns data, `getTemplateName()` returns the template path
|
||||||
|
- What's unclear: Whether the data is automatically available as a template variable, or needs explicit passing
|
||||||
|
- Recommendation: Check `AbstractWidget::render()` or the widget renderer to see how data flows to Twig. If `getData()` result isn't auto-available, the widget can override `getOptions()` to inject projects.
|
||||||
|
|
||||||
|
2. **Does the `/en/` prefix in the timesheet URL match all Kimai installations?**
|
||||||
|
- What we know: Kimai uses locale-prefixed routes. The route name is `timesheet`, resolved via `path('timesheet')` in Twig.
|
||||||
|
- What's unclear: Whether the JS-side URL should be hardcoded with `/en/` or derived from the page context
|
||||||
|
- Recommendation: Pass the timesheet base URL as another `data-` attribute on the container, generated via `{{ path('timesheet') }}` in Twig. This is locale-safe. **This is the correct approach.**
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Framework | Vitest 4.1.3 + jsdom |
|
||||||
|
| Config file | `vitest.config.ts` |
|
||||||
|
| Quick run command | `npm test` |
|
||||||
|
| Full suite command | `npm test` |
|
||||||
|
|
||||||
|
### Phase Requirements to Test Map
|
||||||
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||||
|
|--------|----------|-----------|-------------------|-------------|
|
||||||
|
| HEAT-07 | Click cell navigates to timesheet filtered by date | unit | `npx vitest run --reporter=verbose` | No -- Wave 0 |
|
||||||
|
| HEAT-07 | Click cell with project filter includes project in URL | unit | `npx vitest run --reporter=verbose` | No -- Wave 0 |
|
||||||
|
| INTR-01 | Filter dropdown renders with project options | unit | `npx vitest run --reporter=verbose` | No -- Wave 0 |
|
||||||
|
| INTR-01 | Selecting project re-fetches data and re-renders | unit | `npx vitest run --reporter=verbose` | No -- Wave 0 |
|
||||||
|
| INTR-01 | "All Projects" resets to unfiltered view | unit | `npx vitest run --reporter=verbose` | No -- Wave 0 |
|
||||||
|
| TEST-04 | Click interaction tests exist and pass | unit | `npx vitest run --reporter=verbose` | No -- Wave 0 |
|
||||||
|
| TEST-04 | Tooltip interaction tests exist and pass | unit | `npx vitest run --reporter=verbose` | Partial -- existing tests cover tooltip show/hide |
|
||||||
|
|
||||||
|
### Sampling Rate
|
||||||
|
- **Per task commit:** `npm test`
|
||||||
|
- **Per wave merge:** `npm test` + manual verification in running Kimai
|
||||||
|
- **Phase gate:** Full JS test suite green + PHP tests still passing
|
||||||
|
|
||||||
|
### Wave 0 Gaps
|
||||||
|
- [ ] `assets/test/interaction.test.ts` -- covers HEAT-07 (click navigation) and TEST-04 (click interactions)
|
||||||
|
- [ ] `assets/test/filter.test.ts` -- covers INTR-01 (filter dropdown render, selection, re-fetch)
|
||||||
|
- [ ] Mock utilities for `window.location` and `fetch` -- shared across interaction tests
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- Kimai source code at `dev/kimai/src/Controller/TimesheetController.php` -- verified route path `/timesheet/` and route name `timesheet` [VERIFIED]
|
||||||
|
- Kimai source code at `dev/kimai/src/Form/Type/DateRangeType.php` -- verified daterange format, `DATE_SPACER` constant, and `yyyy-MM-dd` fallback format [VERIFIED]
|
||||||
|
- Kimai source code at `dev/kimai/tests/Controller/AbstractControllerBaseTestCase.php` -- verified daterange test format using `n/j/Y` with ` - ` separator [VERIFIED]
|
||||||
|
- Project source code `assets/src/heatmap.ts` -- verified existing rendering pattern, tooltip handlers, `DayCell` interface [VERIFIED]
|
||||||
|
- Project source code `Controller/HeatmapController.php` -- verified `?project=N` query parameter support [VERIFIED]
|
||||||
|
- Project `package.json` -- verified all dependency versions [VERIFIED]
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- None
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence)
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- Standard stack: HIGH -- no new dependencies, all existing code verified
|
||||||
|
- Architecture: HIGH -- patterns verified against Kimai source and existing codebase
|
||||||
|
- Pitfalls: HIGH -- daterange format verified against actual Kimai code, not assumed
|
||||||
|
- Interaction patterns: HIGH -- d3 click handling is standard API, verified in d3-selection docs
|
||||||
|
|
||||||
|
**Research date:** 2026-04-08
|
||||||
|
**Valid until:** 2026-05-08 (stable -- no fast-moving dependencies)
|
||||||
Loading…
Add table
Reference in a new issue