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:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -91,6 +91,7 @@ function useCurrentShifts(): { current: CurrentShift[]; tills: TillId[]; workabl
|
||||
subscriptionTotalMinor: x.subscriptionTotalMinor,
|
||||
subscriptionSalesMinor: x.subscriptionSalesMinor,
|
||||
subscriptionWindowMinor: x.subscriptionWindowMinor,
|
||||
chargesByModuleMinor: x.chargesByModuleMinor,
|
||||
openingFloatMinor: x.openingFloatMinor,
|
||||
cashAddedMinor: x.cashAddedMinor,
|
||||
cashRemovedMinor: x.cashRemovedMinor,
|
||||
@@ -332,6 +333,24 @@ function ShiftCard({ s, showOperator, showTill, open, selected, onClick }: { s:
|
||||
);
|
||||
}
|
||||
|
||||
/** One figure per module whose money rode this till's tickets (a booth-paid wash) —
|
||||
* nothing when none did, so booth-only sites see the report they always saw. `spacer`
|
||||
* keeps a 2-column grid's pairs aligned. */
|
||||
function ChargeFigures({ charges, cur, spacer }: { charges?: Partial<Record<string, number>>; cur: string | null; spacer?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const rows = Object.entries(charges ?? {}).filter(([, v]) => (v ?? 0) > 0);
|
||||
return (
|
||||
<>
|
||||
{rows.map(([m, v]) => (
|
||||
<Fragment key={m}>
|
||||
<Figure label={t("shift.srcOnTicket", { module: t(`modules.name.${m}`) })} value={money(v ?? 0, cur)} />
|
||||
{spacer && <span />}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ShiftActivityLog({
|
||||
shift,
|
||||
isCurrent,
|
||||
@@ -356,9 +375,10 @@ function ShiftActivityLog({
|
||||
const [detailEvent, setDetailEvent] = useState<LedgerEvent | null>(null);
|
||||
|
||||
// The current shift's log runs entry→now (no upper bound); a closed shift is bounded.
|
||||
// Per till: the booth's log has no wash-desk activity in it, and vice versa.
|
||||
const q = useQuery({
|
||||
queryKey: ["shift-events", shift.id, shift.endedAt],
|
||||
queryFn: () => fetchEvents(1000, shift.startedAt, isCurrent ? undefined : shift.endedAt),
|
||||
queryKey: ["shift-events", shift.id, shift.endedAt, shift.till],
|
||||
queryFn: () => fetchEvents(1000, shift.startedAt, isCurrent ? undefined : shift.endedAt, shift.till),
|
||||
refetchInterval: isCurrent ? 5000 : false,
|
||||
});
|
||||
const events = q.data?.events ?? [];
|
||||
@@ -389,6 +409,7 @@ function ShiftActivityLog({
|
||||
<Figure label={t("shifts.srcSubscriptions")} value={money(shift.subscriptionTotalMinor, cur)} />
|
||||
{/* Abonime is the subscription TOTAL; only the out-of-window part is broken out. */}
|
||||
<Figure label={t("shifts.srcSubWindow")} value={money(shift.subscriptionWindowMinor, cur)} sub />
|
||||
<ChargeFigures charges={shift.chargesByModuleMinor} cur={cur} />
|
||||
<Figure label={t("shifts.openingFloat")} value={money(shift.openingFloatMinor, cur)} />
|
||||
<Figure label={t("shifts.cashTaken")} value={money(shift.cashTotalMinor, cur)} />
|
||||
<Figure label={t("shifts.cashAdded")} value={money(shift.cashAddedMinor, cur)} />
|
||||
@@ -446,6 +467,7 @@ function EndShiftModal({ shift, onClose, onDone }: { shift: ShiftSummary; onClos
|
||||
<span />
|
||||
<Figure label={t("shift.srcTickets")} value={money(report.ticketTotalMinor, report.currency)} />
|
||||
<Figure label={t("shift.srcSubscriptions")} value={money(report.subscriptionTotalMinor, report.currency)} />
|
||||
<ChargeFigures charges={report.chargesByModuleMinor} cur={report.currency} spacer />
|
||||
{/* Abonime is the subscription TOTAL; only the out-of-window part is broken out. */}
|
||||
<span />
|
||||
<Figure label={t("shift.srcSubWindow")} value={money(report.subscriptionWindowMinor, report.currency)} sub />
|
||||
@@ -472,6 +494,7 @@ function EndShiftModal({ shift, onClose, onDone }: { shift: ShiftSummary; onClos
|
||||
<div className="mt-2 grid grid-cols-2 gap-x-6 gap-y-0.5">
|
||||
<Figure label={t("shift.srcTickets")} value={money(shift.ticketTotalMinor, cur)} />
|
||||
<Figure label={t("shift.srcSubscriptions")} value={money(shift.subscriptionTotalMinor, cur)} />
|
||||
<ChargeFigures charges={shift.chargesByModuleMinor} cur={cur} spacer />
|
||||
{/* Abonime is the subscription TOTAL; only the out-of-window part is broken out. */}
|
||||
<span />
|
||||
<Figure label={t("shift.srcSubWindow")} value={money(shift.subscriptionWindowMinor, cur)} sub />
|
||||
@@ -513,6 +536,7 @@ function TakingsModal({ till, onClose }: { till: TillId; onClose: () => void })
|
||||
<span />
|
||||
<Figure label={t("shift.srcTickets")} value={money(x.ticketTotalMinor, x.currency)} />
|
||||
<Figure label={t("shift.srcSubscriptions")} value={money(x.subscriptionTotalMinor, x.currency)} />
|
||||
<ChargeFigures charges={x.chargesByModuleMinor} cur={x.currency} spacer />
|
||||
{/* Abonime is the subscription TOTAL; only the out-of-window part is broken out. */}
|
||||
<span />
|
||||
<Figure label={t("shift.srcSubWindow")} value={money(x.subscriptionWindowMinor, x.currency)} sub />
|
||||
|
||||
Reference in New Issue
Block a user