kimai-plugin-heatmap/.planning/research/FEATURES.md

138 lines
12 KiB
Markdown

# Feature Landscape
**Domain:** Time-tracking heatmap dashboard widget (v1.1 milestone -- modes, filtering, toggles)
**Researched:** 2026-04-08
## Table Stakes
Features that users of a multi-mode heatmap widget would expect once modes are advertised. Missing any of these makes the feature set feel unfinished.
| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Mode switcher UI | Users need a way to switch views. Without it, modes are invisible. | Low | Use Tabler's `nav-segmented` component -- it exists specifically for toggling views within the same context. Maps to `data-bs-toggle="tab"`. Max 4-5 segments. |
| Year-view heatmap (existing) | Already shipped in v1.0. Must remain default mode. | Done | Existing `renderHeatmap()` in `heatmap.ts` |
| Day-of-week aggregation (week mode) | Standard "punchcard" analysis -- which weekdays are busiest. Every time-tracking analytics tool offers this. | Medium | Aggregate existing `DayEntry[]` client-side by `Date.getDay()`. No new backend query needed -- sum/average existing daily data by weekday. 7-row or 7-column chart with color intensity. |
| Hours vs entry-count toggle | The data already has both `hours` and `count` fields. Users expect to choose which metric colors the heatmap. | Low | Toggle the `colorScale` domain between `hours` and `count`. A small segmented control or pair of radio buttons. Applies to all modes. |
| Cascading entity pickers (customer -> project -> activity) | Kimai users see TomSelect pickers everywhere else in the app. A plain `<select>` feels foreign. Once activity filtering is promised, the cascade is expected. | Medium-High | See Complexity Notes below for full analysis of integration approach. |
| Activity filtering | Descoped from v1.0. Now the primary reason for the entity picker upgrade. Users who filter by project will expect activity filtering too. | Medium | Backend: add `?activity=N` param to `HeatmapController::data()`, add `AND t.activity = :activity` clause to `HeatmapService::getDailyAggregation()`. Frontend: wire activity picker into fetch URL. Kimai API route `get_activities` already accepts `?project=N` for cascading. |
| Persistent filter/mode state | Switching modes or filters then refreshing should not reset everything. | Low | Use URL query params (shareable, bookmarkable) or `localStorage`. URL params preferred. |
## Differentiators
Features that elevate this from "yet another heatmap" to a genuinely useful time-analysis tool. Not expected, but valued.
| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| Time-of-day heatmap (day mode) | Shows *when during the day* work happens. Reveals morning-person vs night-owl patterns. Uncommon in time-tracking tools. | High | Requires new backend query: `GROUP BY HOUR(t.begin)` for hour-level aggregation. New data shape: `HourEntry { hour: number, hours: number, count: number }`. New d3 layout: 24-row/column grid. Needs new API response format or a `mode` query param. |
| Combined day/hour matrix | Full punchcard: 7 rows (days) x 24 cols (hours). The GitHub punchcard view. Shows exactly when work happens across the week. | High | Backend: `GROUP BY DAYOFWEEK(t.begin), HOUR(t.begin)`. New data shape: `PunchcardEntry { day: number, hour: number, hours: number, count: number }`. d3 layout: 7x24 grid with circle sizes or color intensity. Most complex visualization but highest insight value. |
| Customer-level filtering | Filter heatmap by customer (one level above project). Useful for seeing client-specific patterns. | Low | The cascade handles this naturally: customer picker drives project picker via `get_projects?customer=N`. Backend `HeatmapService` needs `?customer=N` param that joins through `t.project.customer`. |
| Color scale legend | Small gradient bar showing what colors mean (0h to Xh). | Low | Standard d3 pattern. Horizontal gradient with tick labels. Applies to all modes. |
| Animated transitions between modes | Smooth morphing when switching views (cells rearrange, fade, resize). | Medium | d3 `transition()` API. Key: use consistent `key` functions in `.join()` so d3 matches elements across renders. Polish, not critical. |
## Anti-Features
Things to explicitly NOT build in v1.1.
| Anti-Feature | Why Avoid | What to Do Instead |
|--------------|-----------|-------------------|
| Configurable date range selector | Adds significant UI complexity (date pickers, presets). The fixed 1-year window works for personal use. | Defer to v1.2+. If needed later, a simple "6mo / 1yr / 2yr" segmented control is sufficient. |
| Multi-user comparison | This is a personal tracking tool, not a team dashboard. | Out of scope per PROJECT.md. |
| Export/share heatmap as image | No audience for a personal tool. SVG-to-PNG is fragile. | Out of scope per PROJECT.md. |
| Custom color theme picker | Kimai theme integration is sufficient. Adding a color picker adds options without insight. | Keep hardcoded green scale (matches GitHub convention). |
| Real-time / auto-refresh | Refresh-on-load is fine. WebSocket or polling adds complexity for no gain. | Out of scope per PROJECT.md. |
| Full Kimai form plugin integration | Kimai's `KimaiFormPlugin` lifecycle (`activateForm`/`destroyForm`) is designed for Symfony form pages, not standalone widgets. Coupling to it means depending on Kimai's internal JS architecture. | Use TomSelect directly. Replicate only the cascading pattern. Style with Kimai's existing TomSelect CSS (already loaded globally). See Complexity Notes. |
| Drag-to-select date ranges | Complex interaction, ambiguous purpose (filter? zoom? export?). | Click-to-navigate to timesheet (existing) is sufficient. |
| Drill-down charts within widget | Clicking a day to show a pie chart of projects adds major complexity. | Click-through to Kimai's existing timesheet/report views instead. |
## Feature Dependencies
```
Customer picker -> Project picker -> Activity picker (cascade chain)
Activity picker -> Activity filter param in API (backend support)
Mode switcher UI -> All visualization modes (renders)
Hours/count toggle -> Color scale recalculation (applies to all modes)
Day mode (time-of-day) -> New backend aggregation query (HOUR grouping)
Combined matrix -> New backend aggregation query (DAY + HOUR grouping)
Week mode (day-of-week) -> NO new backend query (aggregate DayEntry[] client-side)
```
## MVP Recommendation
**Phase structure for v1.1:**
Phase 1 -- Mode Switcher + Week Mode + Toggle:
1. **Mode switcher UI** -- Tabler `nav-segmented`, initially Year (existing) + Week modes
2. **Week-mode (day-of-week)** -- Client-side aggregation of existing daily data, no backend changes, proves the multi-mode rendering architecture
3. **Hours/count toggle** -- Small segmented control, applies to all modes, low effort
4. **Rendering refactor** -- Extract shared concerns (SVG container, color scale, tooltip) into base; modes provide layout strategy
Phase 2 -- Entity Pickers + Activity Filtering:
5. **TomSelect entity pickers** -- Replace plain `<select>` with TomSelect instances, wire cascading via Kimai's API routes (`get_projects?customer=N`, `get_activities?project=N`)
6. **Activity filtering** -- Backend `?activity=N` param + frontend wiring
7. **Customer filtering** -- Backend `?customer=N` param (join through project.customer)
Phase 3 -- Advanced Modes (defer or stretch):
8. **Day mode (time-of-day)** -- Needs new backend query + new data shape
9. **Combined day/hour matrix** -- Most complex layout, depends on hour-level backend data
10. **Color scale legend** -- Nice polish, do alongside advanced modes
**Rationale:** Phase 1 proves the multi-mode rendering architecture with zero backend changes. Phase 2 improves filtering UX with Kimai-native patterns and enables activity filtering. Phase 3 adds the data-intensive visualizations requiring new queries. Each phase delivers user-visible value independently.
## Complexity Notes
### TomSelect Integration (the hardest part that looks easy)
Kimai's TomSelect integration runs through a plugin lifecycle: `KimaiFormPlugin` -> `KimaiFormTomselectPlugin` -> `KimaiFormSelect`. The cascading logic in `_activateApiSelects()` (line 386-447 of `KimaiFormSelect.js`) listens for `change` events on selects with `data-related-select` attributes, fetches from `data-api-url` with form field interpolation (`%fieldname%` patterns), and updates the target select via `_updateOptions()` which dispatches `data-reloaded` events.
**Why the plugin cannot use Kimai's system directly:**
- Requires Kimai's `KimaiContainer` DI (for `getPlugin('api')`)
- Requires `KimaiFormPlugin` base class registration
- URL interpolation (`%fieldname%`) assumes Symfony form field naming conventions
- Lifecycle hooks (`activateForm`/`destroyForm`) expect to be called by Kimai's page init
**Two integration approaches:**
**(a) Direct TomSelect instantiation (recommended):**
- Import TomSelect standalone (already loaded in Kimai's global JS)
- Create `<select>` elements styled with Kimai's TomSelect CSS classes
- Implement simplified cascading: on customer change, `fetch('/api/projects?customer=N')`, rebuild project options, trigger activity reload
- Use same option grouping pattern (`parentTitle` for optgroups) as Kimai API responses
- ~100-150 lines of cascading logic, much simpler than Kimai's generic system
**(b) Symfony form fields in Twig template:**
- Render actual `CustomerType`/`ProjectType`/`ActivityType` form fields in the widget's Twig template
- Let Kimai's existing JS auto-enhance them with TomSelect
- Cleaner integration but requires investigation: do Kimai's form plugins activate inside widget Twig blocks?
- Risk: widget Twig templates may not trigger Kimai's form initialization JS
### Rendering Architecture Refactor
Current `renderHeatmap()` is tightly coupled to the year-view grid layout. Adding modes requires a strategy pattern:
- **Shared infrastructure:** SVG container creation, color scale, tooltip, empty state, resize handler
- **Mode-specific:** `generateCells()` / layout function, axis labels, dimensions
- Extract into: `createRenderer(mode)` that returns the appropriate layout strategy
- Each mode is a self-contained module: `yearMode.ts`, `weekMode.ts`, `dayMode.ts`, `combinedMode.ts`
### Backend Aggregation Queries
| Mode | SQL Aggregation | New Backend Code? |
|------|-----------------|-------------------|
| Year (existing) | `GROUP BY DATE(t.date)` | No |
| Week (day-of-week) | Client-side aggregation of daily data | No |
| Day (time-of-day) | `GROUP BY HOUR(t.begin)` | Yes -- new method in `HeatmapService` |
| Combined | `GROUP BY DAYOFWEEK(t.begin), HOUR(t.begin)` | Yes -- new method in `HeatmapService` |
Note: day and combined modes need `t.begin` (timesheet start datetime), not `t.date`. Verify `t.begin` is available and contains time component in Kimai's Timesheet entity.
## Sources
- Tabler segmented control: https://docs.tabler.io/ui/components/segmented-control
- GitHub PunchCard visualization: https://github.com/vnau/punchcard
- Segmented control UX best practices: https://mobbin.com/glossary/segmented-control
- Apple HIG segmented controls: https://developer.apple.com/design/human-interface-guidelines/segmented-controls
- d3 heatmap patterns: https://d3-graph-gallery.com/heatmap.html
- Kimai form select cascading: local `dev/kimai/assets/js/forms/KimaiFormSelect.js` (lines 386-447)
- Kimai API routes: `get_projects` at `src/API/ProjectController.php:56`, `get_activities` at `src/API/ActivityController.php:54`
- Kimai SelectWithApiDataExtension: local `dev/kimai/src/Form/Extension/SelectWithApiDataExtension.php`
- TomSelect: https://tom-select.js.org/