# Technology Stack: v1.1 Additions **Project:** Kimai Heatmap Plugin v1.1 **Researched:** 2026-04-08 **Scope:** NEW stack additions only. Existing validated stack (PHP 8.2, Symfony 6.4, d3 v7, TypeScript, esbuild, Vitest, PHPUnit) is not re-evaluated. ## New Dependencies ### TomSelect (Entity Pickers) | Technology | Version | Purpose | Why | Confidence | |------------|---------|---------|-----|------------| | tom-select | ^2.4.3 | Customer/project/activity cascading pickers | Kimai uses TomSelect 2.4.3 for all its entity pickers. Matching the version ensures visual consistency with Kimai's existing selects. ~16KB gzipped complete, ~12KB base. | HIGH | | @types/tom-select | latest | TypeScript definitions | Type safety for TomSelect API calls (constructor options, instance methods like `clear()`, `sync()`, `destroy()`). | HIGH | **Why bundle TomSelect ourselves instead of reusing Kimai's instance:** Kimai bundles TomSelect inside its Webpack Encore build (`app` entry point). It is NOT exposed as a `window` global. Our plugin ships as a standalone IIFE via esbuild, loaded separately from Kimai's bundle. There is no way to import from Kimai's Webpack chunks at runtime. Verified by examining: `dev/kimai/webpack.config.js` (no `externals` or global exposure), `dev/kimai/assets/js/forms/KimaiFormSelect.js` (imports TomSelect as ESM module), and `dev/kimai/assets/js/KimaiLoader.js` (all form plugins are internal to the Kimai container system). **CSS consideration:** Kimai already loads TomSelect's CSS via its Sass pipeline. Our TomSelect instances will inherit Kimai's existing `.ts-wrapper`, `.ts-control`, `.ts-dropdown` styles automatically. We do NOT need to bundle TomSelect CSS -- only the JS. **Import strategy:** Use `tom-select/dist/js/tom-select.complete` (includes change_listener plugin needed for cascading) or cherry-pick `tom-select/src/tom-select` + specific plugins. The complete bundle is fine at 16KB gzipped given we already bundle d3 modules. ### No New d3 Modules Needed The existing d3 dependencies are sufficient for all four visualization modes: | Mode | Layout Approach | d3 Modules Used | |------|----------------|-----------------| | Year (existing) | Week columns x 7 day rows | d3-selection, d3-scale, d3-time, d3-time-format, d3-array | | Week | 7 columns (Mon-Sun), single row of aggregated values | d3-selection, d3-scale, d3-array | | Day (time-of-day) | 24 columns (hours), 7 rows (days-of-week) | d3-selection, d3-scale, d3-array | | Combined (day/hour) | Date columns x 24 hour rows, or 7 day-of-week columns x 24 hour rows | d3-selection, d3-scale, d3-array, d3-time | All modes render SVG rect grids -- the same pattern as the year view. The difference is in data aggregation (backend) and grid layout math (frontend), not in d3 capabilities. `d3-shape` (listed in v1.0 STACK.md) was never added to package.json and is not needed. Rectangles are drawn with `rect` elements via d3-selection, not d3-shape. ## Backend Additions ### New Query Aggregations The existing `HeatmapService::getDailyAggregation()` groups by `DATE(t.date)`. New modes need: | Mode | SQL Aggregation | New Method | |------|----------------|------------| | Week (day-of-week) | `GROUP BY DAYOFWEEK(t.date)` | `getWeekdayAggregation()` | | Day (time-of-day) | `GROUP BY HOUR(t.begin)` | `getHourlyAggregation()` | | Combined | `GROUP BY DAYOFWEEK(t.date), HOUR(t.begin)` | `getDayHourAggregation()` | These are Doctrine DQL queries using the same `TimesheetRepository` and `QueryBuilder` pattern as the existing method. No new PHP packages needed. **Note:** Kimai stores `t.begin` (start time) and `t.end` (end time) on timesheet entries. For hourly breakdown, use `HOUR(t.begin)` to assign each entry to its starting hour. Multi-hour entries will be attributed to their start hour for simplicity; splitting across hours would require duration-proportional allocation (complex, defer to future). ### New Controller Endpoints The existing `HeatmapController::data()` endpoint needs: - `mode` query param (`year|week|day|combined`, default `year`) - `customer` query param (for cascading filter support) - `activity` query param (for activity filtering) These are additions to the existing controller, not new bundles or packages. ### Entity Cascade Endpoints **Decision: Use our own endpoints, NOT Kimai's API routes.** Kimai's API routes (`get_customers`, `get_projects`, `get_activities`) are guarded by `#[IsGranted('API')]`, which requires the user to have the API permission. Dashboard widget users may not have API access. Additionally, Kimai's cascading logic lives in `KimaiFormSelect.js` which depends on `KimaiContainer.getPlugin('api')` -- the entire Kimai plugin system that our standalone widget cannot access. Instead, add lightweight endpoints to our own `HeatmapController`: - `GET /heatmap/customers` -- customers the user has timesheet entries for - `GET /heatmap/projects?customer={id}` -- projects filtered by customer - `GET /heatmap/activities?project={id}` -- activities filtered by project These use `IS_AUTHENTICATED_REMEMBERED` + `view_own_timesheet` (same as our existing data endpoint) and query only entities the user has actually tracked time against. This is better UX anyway -- no empty customers/projects cluttering the pickers. ## UI Additions ### Mode Switcher Use Tabler's `btn-group` (segmented control) for mode switching. Tabler is Kimai's UI framework and is already loaded. No additional CSS or JS framework needed. ```html