feat(reports): admin Reports dashboard — ledger-first charts
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
This commit is contained in:
@@ -308,6 +308,66 @@ export function testAnpr(driverId: string, config: DeviceConfig): Promise<AnprTe
|
||||
});
|
||||
}
|
||||
|
||||
// --- Admin reports -------------------------------------------------------
|
||||
export type ReportBucket = "hour" | "day" | "month";
|
||||
|
||||
export interface ReportSeriesPoint {
|
||||
bucket: string;
|
||||
entries: number;
|
||||
exits: number;
|
||||
revenueMinor: number;
|
||||
payments: number;
|
||||
}
|
||||
|
||||
export interface ReportTotals {
|
||||
entries: number;
|
||||
exits: number;
|
||||
payments: number;
|
||||
revenueMinor: number;
|
||||
cashMinor: number;
|
||||
cardMinor: number;
|
||||
ticketMinor: number;
|
||||
subscriptionSalesMinor: number;
|
||||
subscriptionWindowMinor: number;
|
||||
closedSessions: number;
|
||||
totalParkedMinutes: number;
|
||||
avgParkedMinutes: number;
|
||||
medianParkedMinutes: number;
|
||||
}
|
||||
|
||||
export interface ReportSubscriptionStats {
|
||||
active: number;
|
||||
suspended: number;
|
||||
revoked: number;
|
||||
currentlyValid: number;
|
||||
coveredCars: number;
|
||||
}
|
||||
|
||||
export interface ReportSummary {
|
||||
from: string;
|
||||
to: string;
|
||||
bucket: ReportBucket;
|
||||
tz: string;
|
||||
currency: string | null;
|
||||
totals: ReportTotals;
|
||||
series: ReportSeriesPoint[];
|
||||
entriesByHour: number[];
|
||||
subscriptions: ReportSubscriptionStats;
|
||||
}
|
||||
|
||||
/** The whole admin dashboard (totals + series + peak-hours + subscriptions) for a range. */
|
||||
export function fetchReport(from: string, to: string, bucket: ReportBucket): Promise<ReportSummary> {
|
||||
const qs = new URLSearchParams({ from, to, bucket }).toString();
|
||||
return apiFetch<ReportSummary>(`/api/reports/summary?${qs}`);
|
||||
}
|
||||
|
||||
/** URL for the CSV export of the per-bucket series (opened/downloaded directly; the
|
||||
* auth cookie rides along same-origin). */
|
||||
export function reportCsvUrl(from: string, to: string, bucket: ReportBucket): string {
|
||||
const qs = new URLSearchParams({ from, to, bucket }).toString();
|
||||
return apiUrl(`/api/reports/summary.csv?${qs}`);
|
||||
}
|
||||
|
||||
export interface BackendIpCandidate {
|
||||
ip: string;
|
||||
iface: string;
|
||||
|
||||
Reference in New Issue
Block a user