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
+20
View File
@@ -2088,3 +2088,23 @@ modal shows Total + "Pay + open barrier" with NO tender/cash/card row. Re-enable
a bank-certified P2PE terminal is provisioned (PCI scope stays out of the app — the terminal captures
card data, not the app). New page concepts/card-payments.md documents current state + future-POS
device requirements + re-enable path; linked from index, parking-session, open-questions #3.
## [2026-07-01] feat | Drawer redesign — operator records freely, admin reviews after; moved to /drawer
Reworked drawer cash movements from synchronous admin-authorization-at-creation (operator typed an
admin's password inline at the booth for every receipt/disbursement) to operator-records → admin-
reviews-after. An operator with drawer:create RECORDS a cash_in/cash_out freely; it counts in the
drawer immediately. An admin with drawer:review AUTHORIZES/DENIES it after via a new signed cash_review
event { refId, decision, reviewedBy, note? }. THE LOAD-BEARING CHOICE (settled with user): a denial is
a FLAG, not a reversal — it never appends reversing cash and never touches the drawer balance (the
correction is the admin's/accountant's job outside the app; we are NOT building accounting). This kills
the cross-shift-leak problem the user raised: a denial that lands after the reviewed shift closed can't
pollute the next operator's inherited drawer, because it moves no cash. New `drawer` resource +
drawer:create (per-role revocable) / drawer:review permissions; migration 0018 grants operator
drawer:create. Feature moved OFF the polluted /shifts route to a top-level /drawer (operator: record +
own; admin: review queue + all). New routes/drawer.ts (lifted from routes/shift.ts, retired the
authorizer-password gate; kept shift:cash for its other job = admin-sees-all-shifts scope),
DrawerManager.tsx, drawer.* i18n (sq+en). Verified: full monorepo build/lint/test green (225 server
tests incl. the op1-denied → op2-drawer-unchanged regression); Playwright end-to-end on /drawer
(record disbursement → pending → authorize → status flips, ledger shows cash_out + cash_review with no
authorizedBy). Recorded in shift.md "Drawer review".