feat(drawer): operator records cash movements, admin reviews after (own /drawer route)

Rework drawer cash movements from synchronous admin-authorization-at-creation
(operator typed an admin's password inline for every receipt/disbursement) to
operator-records-freely -> admin-reviews-after.

- New `drawer` resource: drawer:create (operator records; admin-revocable per
  role) + drawer:review (admin authorizes/denies). Migration 0018 grants the
  default operator role drawer:create; admin gets all in code.
- New signed `cash_review` ledger event { refId, decision, reviewedBy, note? }.
  A DENIAL is a FLAG, not a reversal: it never appends reversing cash and never
  touches the drawer balance (the correction is settled outside the app). This
  is what keeps a late review from leaking into the next operator's inherited
  drawer — a denial that lands after the reviewed shift closed moves no cash.
  Regression test: op1 disburses -> closes -> op2 inherits -> admin denies ->
  op2 drawer unchanged.
- Move the feature OFF the polluted /shifts route to a top-level /drawer
  (operator: record + own; admin: review queue + all). routes/drawer.ts lifted
  from routes/shift.ts (retired the authorizer-password gate; kept shift:cash
  for its other job = admin-sees-all-shifts). New DrawerManager.tsx.

Display fixes bundled:
- Render cash_review in the event-detail modal (decision / reviewed-by / note /
  movement ref) — previously showed nothing.
- Relabel the shift drawer figures for clarity: Daily takings / Receipts /
  Disbursements (was Cash payments / Cash added / Cash removed).
- Hide the Card figure everywhere when CARD_PAYMENTS_ENABLED is false (no POS
  on-site), matching the card-tender gate.

shared/db/server/web all typecheck; 225 server tests pass (incl. the drawer
review + cross-shift-leak regression); web build + i18n parity green. Verified
end-to-end via Playwright. Recorded in wiki/concepts/shift.md.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-01 11:17:20 +02:00
parent 018328a877
commit 114a32e6f2
18 changed files with 879 additions and 206 deletions
+42 -6
View File
@@ -58,12 +58,42 @@ export const en: Catalog = {
users: "Users",
roles: "Roles",
shifts: "Shifts",
drawer: "Drawer",
reports: "Reports",
recycleBin: "Recycle bin",
logs: "Logs",
backup: "Backup",
profile: "Profile",
},
drawer: {
recordTitle: "Record a cash movement",
amount: "amount",
reasonPlaceholder: "reason (e.g. supplier payment, bank drop)",
recordHint: "Recorded to the drawer immediately. An admin reviews it afterward.",
mandatArketimi: "Receipt (in) +",
mandatPagese: "Disbursement (out) −",
enterPositive: "Enter a positive amount.",
recorded: "{{no}} recorded. Drawer now {{amount}}.",
myTitle: "My cash movements",
allTitle: "Cash movements",
pendingCount: "{{count}} pending",
filterAll: "All",
empty: "No cash movements yet.",
colWhen: "When",
colType: "Type",
colAmount: "Amount",
colReason: "Reason",
colOperator: "Operator",
colStatus: "Status",
status: {
pending: "pending",
authorized: "authorized",
denied: "denied",
},
authorize: "Authorize",
deny: "Deny",
denyNotePlaceholder: "reason for denial (optional)",
},
profile: {
title: "My profile",
accountSection: "Account",
@@ -183,6 +213,8 @@ export const en: Catalog = {
evtCashMovement: "CASH",
evtCashIn: "PAY-IN",
evtCashOut: "PAY-OUT",
evtCashReview: "REVIEW",
decision: { authorize: "authorized", deny: "denied" },
evtAnomaly: "ANOMALY",
evtRefused: "REFUSED",
// live-feed event detail line + classification badges (computed from payload)
@@ -224,6 +256,10 @@ export const en: Catalog = {
edPlate: "Plate",
edCategory: "Category",
edOperator: "Operator",
edDecision: "Review decision",
edReviewedBy: "Reviewed by",
edReviewNote: "Note",
edReviewRef: "Movement ref",
edTariffVersion: "Tariff version",
edRawPayload: "Raw signed payload",
edOccurrence: "Occurrence id",
@@ -709,9 +745,9 @@ export const en: Catalog = {
srcSubWindow: "out-of-window",
drawerSection: "— Drawer —",
openingFloat: "Opening cash:",
cashTaken: "Cash taken:",
cashAdded: "Cash added:",
cashRemoved: "Cash removed:",
cashTaken: "Daily takings:",
cashAdded: "Receipts:",
cashRemoved: "Disbursements:",
expectedDrawer: "Expected drawer:",
printedToReceipt: "Printed to booth receipt.",
recordedNoPrinter: "Recorded (no printer to print to).",
@@ -760,9 +796,9 @@ export const en: Catalog = {
current: "current",
drawerSection: "Drawer",
openingFloat: "Opening cash",
cashTaken: "Cash taken",
cashAdded: "Cash added",
cashRemoved: "Cash removed",
cashTaken: "Daily takings",
cashAdded: "Receipts",
cashRemoved: "Disbursements",
loadFailed: "Failed to load shifts.",
},
reports: {