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

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:

  • 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.

<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