feat(tills): per-till activity log, wash bucket on the booth Z-report, wash-desk printer role
Closes the three known follow-ups of the Tills decision (venue-modules.md): - Activity log per till: `tillOfEvent(type, payload)` in @parking/shared (money events by payload till, other events by their owning module's till, everything else booth), applied by `/api/events?till=` in SQL and passed by the hub log, the Drawer "today" panel and the booth feed (history + live pushes). The events route admits a role that holds a module feed permission without event:read and returns only that module's event types — the live-socket rule. - Booth Z-report: `chargesByModuleMinor` sums the chargeLines on the till's payments by module; the ticket bucket excludes them (Bileta = parking only); printed "Lavazh (në biletë)" only when any was taken. The wash till's slip prints "Lavazh:". - Printer role `wash-desk`: the wash till's Z-report and vouchers print there, falling back to the booth printer; nothing falls back to the desk. `printerRoleOf()` is the one reading of the role field (the entry/booth loaders treated any non-booth role as an entry dispenser). Footer label "at wash desk". Also: `GET /api/carwash/settings` opens to carwash:read OR site:read (new requireAnyPermission) — the Wash operator job could not load the desk's category and service pickers. Tests for all four; wiki (shift, printer-roles-failover, venue-modules, log) updated. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
@@ -13,6 +13,7 @@ import { BoothPayModal } from "./BoothPayModal.js";
|
||||
import { ActiveSessions } from "./ActiveSessions.js";
|
||||
import { FilterBar, SegGroup, type SegOption } from "./ui/FilterBar.js";
|
||||
import { EventDetailModal, EventRow } from "./ui/event-detail.js";
|
||||
import { tillOfEvent } from "@parking/shared";
|
||||
|
||||
// The live operator booth view — the real-time heart of the console. Occupancy
|
||||
// gauge + a streaming entry/exit/payment ticker. Query owns the initial load and
|
||||
@@ -246,7 +247,7 @@ export function BoothScreen() {
|
||||
const occQuery = useQuery({ queryKey: qk.occupancy, queryFn: fetchOccupancy });
|
||||
const eventsQuery = useQuery({
|
||||
queryKey: [...qk.events, shiftStart ?? "none"],
|
||||
queryFn: () => fetchEvents(100, shiftStart ?? undefined),
|
||||
queryFn: () => fetchEvents(100, shiftStart ?? undefined, undefined, "booth"),
|
||||
enabled: shiftOpen,
|
||||
});
|
||||
|
||||
@@ -275,13 +276,15 @@ export function BoothScreen() {
|
||||
|
||||
// Merge: live events first (newest), then the queried history, de-duped by id —
|
||||
// then clip to the current shift window (the live store spans shifts; the feed
|
||||
// must not show events from before this shift's start). No shift → no feed.
|
||||
// must not show events from before this shift's start) and to the BOOTH till (the
|
||||
// socket also pushes wash-desk events to anyone with carwash:read; they are the wash
|
||||
// shift's activity, not this one's — tillOfEvent). No shift → no feed.
|
||||
const seen = new Set(liveFeed.map((e) => e.id));
|
||||
const history = (eventsQuery.data?.events ?? []).filter((e) => !seen.has(e.id));
|
||||
const merged = [...liveFeed, ...history].slice(0, 200);
|
||||
const scoped =
|
||||
shiftOpen && shiftStart
|
||||
? merged.filter((e) => e.occurredAt >= shiftStart)
|
||||
? merged.filter((e) => e.occurredAt >= shiftStart && tillOfEvent(e.type, e.payload) === "booth")
|
||||
: [];
|
||||
|
||||
// Apply the live-feed filters. Source maps to booth (operator-initiated `manual`)
|
||||
|
||||
Reference in New Issue
Block a user