feat(reports): occupancy curve, hour×dow heatmap, stay histogram, fraud KPIs

The dashboard had generic BI views but nothing parking-shaped. Added:

- Occupancy step-area over the range with the configured capacity as a
  red reference line. occupancyStart folds the ENTIRE prior ledger
  (voided entries excluded, clamped ≥0); each series point carries
  occupancyEnd. Answers "when are we near full".
- Entries heatmap hour × day-of-week (7×24, row 0 = Monday, site tz) as
  a pure CSS-grid intensity map — weekday-vs-weekend at a glance, the
  direct evidence for tariff windows. Replaces the flat hour histogram
  (strictly contains it).
- Stay-duration histogram at tariff-shaped edges (30m/1h/2h/4h/8h/24h/
  tail): where ladder/up-to breakpoints should sit.
- Voids + anomalies KPIs (accented when >0) — the look-closer counters
  the signed chain exists for; peak-occupancy KPI (peak / capacity).
- Revenue bars stacked cash vs card (the drawer's money vs the bank's);
  CSV export gains cash, card, occupancy_end columns.

Internals: localParts caches its Intl formatter per tz (was one new
formatter per ledger row); @parking/db re-exports lt/gt. 5 new tests.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-06 12:35:54 +02:00
parent a2e102f3dd
commit 7ef332999e
9 changed files with 334 additions and 32 deletions
+12
View File
@@ -483,7 +483,11 @@ export interface ReportSeriesPoint {
entries: number;
exits: number;
revenueMinor: number;
cashMinor: number;
cardMinor: number;
payments: number;
/** Cars inside at the END of the bucket. */
occupancyEnd: number;
}
export interface ReportTotals {
@@ -500,6 +504,8 @@ export interface ReportTotals {
totalParkedMinutes: number;
avgParkedMinutes: number;
medianParkedMinutes: number;
voids: number;
anomalies: number;
}
export interface ReportSubscriptionStats {
@@ -519,6 +525,12 @@ export interface ReportSummary {
totals: ReportTotals;
series: ReportSeriesPoint[];
entriesByHour: number[];
/** 7×24, row 0 = Monday — entries heatmap (weekday-vs-weekend patterns). */
entriesByDowHour: number[][];
/** Stay-duration histogram; last bucket has uptoMin null (>24h tail). */
stayHistogram: { uptoMin: number | null; count: number }[];
occupancyStart: number;
capacity: number | null;
subscriptions: ReportSubscriptionStats;
}