# 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
``` ### Display Toggle (Hours vs Entry Count) Same pattern -- `btn-group` or a simple toggle. The data is already in `DayEntry` (`hours` and `count` fields). This is purely a frontend change to switch which field drives the color scale. ## What NOT to Add | Temptation | Why Not | |------------|---------| | `d3-axis` | The new modes don't need formal axes. Simple text labels (like the existing month/day labels) are sufficient and lighter. | | `d3-shape` | We draw rectangles with ``, not path generators. | | `d3-transition` | Animations between mode switches would be nice but add complexity. Defer to polish. | | `chart.js` or `cal-heatmap` | Same rationale as v1.0 -- raw d3 gives us full control over Kimai theme integration. | | TomSelect CSS bundle | Already loaded by Kimai's Sass pipeline. Bundling it would cause style conflicts. | | Kimai's `KimaiFormSelect.js` | Depends on `KimaiContainer` plugin system. Our widget is standalone IIFE. | | `luxon` or `date-fns` | d3-time and d3-time-format handle all our date math. No need for another date library. | ## Updated Type Definitions New types needed in `types.ts`: ```typescript // Visualization modes type HeatmapMode = 'year' | 'week' | 'day' | 'combined'; // Display metric toggle type DisplayMetric = 'hours' | 'count'; // Hourly data for day/combined modes interface HourEntry { hour: number; // 0-23 dayOfWeek: number; // 0-6 (Monday=0) hours: number; count: number; } // Weekday aggregation for week mode interface WeekdayEntry { dayOfWeek: number; // 0-6 hours: number; count: number; } // Entity picker options interface CustomerOption { id: number; name: string; } interface ActivityOption { id: number; name: string; } ``` ## Installation Commands ```bash # New runtime dependency npm install tom-select@^2.4.3 # New dev dependency (check if types ship with tom-select itself first) npm install -D @types/tom-select ``` No new PHP/Composer dependencies required. ## esbuild Consideration TomSelect's package includes CSS files. When importing from `tom-select`, esbuild may try to bundle CSS. Since Kimai already loads TomSelect CSS, import only the JS entry point: ```typescript import TomSelect from 'tom-select/dist/js/tom-select.complete'; ``` This avoids CSS duplication and style conflicts. ## Sources - Kimai TomSelect integration: `dev/kimai/assets/js/forms/KimaiFormSelect.js` (local source, verified) - Kimai webpack config: `dev/kimai/webpack.config.js` (local source, confirmed no TomSelect global exposure) - Kimai API permissions: `dev/kimai/src/API/ProjectController.php` (local source, `#[IsGranted('API')]`) - Kimai API cascading pattern: `dev/kimai/src/Form/Extension/SelectWithApiDataExtension.php` (local source) - Kimai package.json: `dev/kimai/package.json` (local source, `tom-select: ^2.4.3`) - [tom-select on npm](https://www.npmjs.com/package/tom-select) -- v2.5.2 latest, ~16KB gzipped - [tom-select v2.5.2 on Bundlephobia](https://bundlephobia.com/package/tom-select) -- bundle size analysis - [TomSelect documentation](https://tom-select.js.org/)