feat(shift): two-pane shift history — list + per-shift activity log, timeframe presets
Rework the shift screen into a master/detail view on /shift: the shift CONTROL (open/close, drawer vouchers, X-report) on top, then a two-pane history below — shift list on the LEFT, the selected shift's signed activity log on the RIGHT. - Timeframe presets replace the bare from/to inputs: Yesterday / Last week / Last month / All / Custom (custom reveals the date pickers). Filters the shift list by start time. - Activity log = every ledger event in the selected shift's [start, end] window (entries, exits, payments, vouchers, anomalies, the Z-report), rendered like the booth live feed (same EVENT_STYLE), with the shift's drawer reconciliation in the pane header. - Scope unchanged + enforced SERVER-SIDE: an operator sees only their own shifts (no operator filter); an admin (shift:cash) sees all + the operator filter. The list auto-selects the newest shift. API: /api/events gains an optional `until` (ISO) upper bound so a shift's window can be fetched ([start,end]); fetchEvents passes it. Verified on live data: a closed shift window returns just its 20 events out of 260. Build+lint 12/12 (i18n parity). The same component also backs /setup/shifts. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { desc, gte, ledgerEvents, type Db } from "@parking/db";
|
||||
import { and, desc, gte, lte, ledgerEvents, type Db } from "@parking/db";
|
||||
import type { LedgerEvent } from "@parking/shared";
|
||||
import { requirePermission } from "../auth.js";
|
||||
import { enrichEvents } from "../event-enrich.js";
|
||||
@@ -19,19 +19,26 @@ export async function eventRoutes(
|
||||
const guard = requirePermission("event:read");
|
||||
|
||||
// Recent events, newest first. `limit` caps the page (default 100, max 1000).
|
||||
// Optional `since` (ISO) scopes the page to events at/after that instant — the
|
||||
// booth passes the current shift's start so the live feed shows ONLY this shift's
|
||||
// activity (logs are per-shift, not all history). See wiki/concepts/shift.md.
|
||||
app.get<{ Querystring: { limit?: string; since?: string } }>(
|
||||
// Optional `since` (ISO) scopes to events at/after that instant — the booth passes
|
||||
// the current shift's start so the live feed shows ONLY this shift's activity. An
|
||||
// optional `until` (ISO) closes the upper bound — the shift-history screen passes a
|
||||
// selected shift's [start, end] to show just that shift's signed activity log.
|
||||
// (logs are per-shift, not all history). See wiki/concepts/shift.md.
|
||||
app.get<{ Querystring: { limit?: string; since?: string; until?: string } }>(
|
||||
"/api/events",
|
||||
{ preHandler: guard },
|
||||
async (req) => {
|
||||
const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 1000);
|
||||
const since = (req.query.since ?? "").trim();
|
||||
const until = (req.query.until ?? "").trim();
|
||||
const bounds = [
|
||||
since ? gte(ledgerEvents.occurredAt, since) : undefined,
|
||||
until ? lte(ledgerEvents.occurredAt, until) : undefined,
|
||||
].filter(Boolean);
|
||||
const rows = db
|
||||
.select()
|
||||
.from(ledgerEvents)
|
||||
.where(since ? gte(ledgerEvents.occurredAt, since) : undefined)
|
||||
.where(bounds.length ? and(...bounds) : undefined)
|
||||
.orderBy(desc(ledgerEvents.index))
|
||||
.limit(limit)
|
||||
.all();
|
||||
|
||||
Reference in New Issue
Block a user