Adds an admin Reports screen (/setup/reports, gated report:read) — an on-demand dashboard over the signed event log. Server (ledger-first): GET /api/reports/summary?from&to&bucket aggregates in one call — entry/exit counts + all money summed straight from ledger_events (same source the shift Z-report reconciles, so totals tie out to the drawer); revenue split into ticket / subscription-sale / out-of-window mirrors the Z-report. Duration stats come from the sessions cache (flagged). All bucketing is in the SITE timezone (siteTz). A .csv export of the per-bucket series. reports.ts + routes/reports.ts. Web: Reports.tsx — date-range presets (today/7d/30d/90d), hour/day/month grain, KPI cards, entry/exit line, revenue bar + cash/card split, revenue-mix pie, peak-hours histogram, numeric breakdown, subscription stats. Charts via Recharts (MIT), lazy-loaded into its own chunk (~111KB gz) so the booth bundle is untouched. New Setup tab + nav + i18n (sq + en parity). asc() exported from @parking/db; formatMinutes helper. Tests: reports.test.ts (10) pin the sums, tz bucketing, money split, duration stats, subscription counts. server 90/90; build+lint 14/14. Wiki: reporting-analytics.md "Built v1" section + log entry. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
4.6 KiB
type, tags, sources, updated, status
| type | tags | sources | updated | status | ||||
|---|---|---|---|---|---|---|---|---|
| concept |
|
2026-06-22 | open |
Reporting & Analytics
Turning the signed event log into the numbers an owner runs the business on. All reports are projections over the append-only-event-chain — the chain is the single source, reports are derived and rebuildable, never a separate ledger.
Built — admin Reports dashboard v1 (2026-06-22)
A first cut shipped: an admin Reports screen (/setup/reports, gated on report:read), an
on-demand dashboard (not a live feed). Server aggregates everything in one call
(GET /api/reports/summary?from&to&bucket) so the SPA only renders; …/summary.csv exports the
per-bucket series. Code: apps/server/src/reports.ts (+ routes/reports.ts), apps/web/src/Reports.tsx.
- Ledger-first (decision 2026-06-22). Entry/exit counts and all money are summed
straight from the signed
ledger_events— the SAME source theshift_z_reportreconciles, so a chart total always ties out to the drawer. The revenue split (transient ticket / subscription sale / out-of-window window-charge) mirrors the Z-report's split exactly (subscriptionSale/subscriptionWindowChargepayload flags). Duration/occupancy stats are the one exception: read from the derivedsessionscache (pairing each entry with its exit on the chain by hand is awkward) — flagged as a cache, not the financial truth. - Site-timezone bucketing. A "day"/"hour" bucket is local wall-clock in
siteConfig.timezone(reusessiteTz()), so a 23:30Z entry lands on the right local date and the peak-hour histogram reads in wall-clock. Bucket grain: hour / day / month, with date-range presets (today / 7d / 30d / 90d). - Views: KPI cards (entries, exits, revenue, payments, avg stay, current subscribers); entry/exit
line; revenue bar (per bucket) + cash/card split; revenue-mix pie; peak-hours histogram
(entries by local hour-of-day); a numeric breakdown (cash/card, the 3-way revenue split, closed
sessions, avg/median stay, active subs + cars covered); subscription status counts + currently-valid
coverage as of the range end. Charts via Recharts (MIT), lazy-loaded into its own bundle
chunk so the booth never downloads it. Tested:
reports.test.ts(10) pin the sums, the tz bucketing, the money split, duration stats, and subscription counts.
Not yet (deferred from the list below): anomalies/voids reporting, per-operator takings, the
plate/entry search (next section), PDF export, and a live dashboard. The report:read permission
already existed for "events feed, occupancy, future reports" — this is its first real consumer
beyond the feed.
Reports (driven by the events already designed)
- Revenue — by day/week/shift, by tender (cash vs. card), gross vs. discounts vs. net. Source:
paymentevents + validation-discounts events +shift_z_report(shift). - Occupancy — current (capacity-occupancy) and historical curve; peak times; turnover.
- Stay analytics — average/median duration, distribution; transient vs. subscription split.
- Permit usage — active permits, utilisation, concurrency vs.
maxConcurrent. - Anomalies — out-of-band opens, never-exited sessions, occupancy drift, over-validation —
the
anomalyevents + reconciliation findings (reconciliation).
Plate / entry search (admin lookup) — user-requested 2026-06-15
The admin can search for an entry/session by licence plate — if the plate was captured (by the opencv-anpr-service or an LPR read; a pure-ticket transient has no plate). Returns the matching session(s): entry/exit times, fee, payment, snapshot image. Useful for disputes ("I was charged for a car that left earlier"), lost-ticket lookup, and incident review.
- Search keys: plate (when captured), ticket id, session id, time range.
- Read-only over the chain; surfaces the linked snapshot (lpr-camera
imageRef) as evidence. - Honest limit: no plate → no plate-search hit. The UI must say "not captured", not "no such car", so the absence isn't mistaken for a missing record.
Properties
- Offline (offline-first): all computed locally from the local DB; no cloud BI dependency.
- Reproducible: a report run twice over the same chain gives the same answer; figures trace to signed events.
- Export for reconciliation / accounting (CSV/PDF) — the periodic external-authority path (open-questions #4).
Open
- Which reports matter at launch vs. later; the export format/cadence.
- Dashboard (live) vs. on-demand reports.