8.7 KiB
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:
modequery param (year|week|day|combined, defaultyear)customerquery param (for cascading filter support)activityquery 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 forGET /heatmap/projects?customer={id}-- projects filtered by customerGET /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.
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary active">Year</button>
<button class="btn btn-sm btn-outline-primary">Week</button>
<button class="btn btn-sm btn-outline-primary">Day</button>
<button class="btn btn-sm btn-outline-primary">Combined</button>
</div>
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 <rect>, 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:
// 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
# 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:
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 -- v2.5.2 latest, ~16KB gzipped
- tom-select v2.5.2 on Bundlephobia -- bundle size analysis
- TomSelect documentation