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

179 lines
8.7 KiB
Markdown

# 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
<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`:
```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/)