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:
@@ -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: {
|
||||
|
||||
@@ -60,12 +60,42 @@ export const sq = {
|
||||
users: "Përdoruesit",
|
||||
roles: "Rolet",
|
||||
shifts: "Turnet",
|
||||
drawer: "Arka",
|
||||
reports: "Raportet",
|
||||
recycleBin: "Koshi",
|
||||
logs: "Loget",
|
||||
backup: "Kopje rezervë",
|
||||
profile: "Profili",
|
||||
},
|
||||
drawer: {
|
||||
recordTitle: "Regjistro një lëvizje arke",
|
||||
amount: "shuma",
|
||||
reasonPlaceholder: "arsyeja (p.sh. pagesë furnitori, depozitë banke)",
|
||||
recordHint: "Regjistrohet menjëherë në arkë. Një admin e shqyrton më pas.",
|
||||
mandatArketimi: "Arkëtim (hyrje) +",
|
||||
mandatPagese: "Pagesë (dalje) −",
|
||||
enterPositive: "Fut një shumë pozitive.",
|
||||
recorded: "{{no}} u regjistrua. Arka tani {{amount}}.",
|
||||
myTitle: "Lëvizjet e mia të arkës",
|
||||
allTitle: "Lëvizjet e arkës",
|
||||
pendingCount: "{{count}} në pritje",
|
||||
filterAll: "Të gjitha",
|
||||
empty: "Asnjë lëvizje arke ende.",
|
||||
colWhen: "Kur",
|
||||
colType: "Lloji",
|
||||
colAmount: "Shuma",
|
||||
colReason: "Arsyeja",
|
||||
colOperator: "Operatori",
|
||||
colStatus: "Statusi",
|
||||
status: {
|
||||
pending: "në pritje",
|
||||
authorized: "autorizuar",
|
||||
denied: "refuzuar",
|
||||
},
|
||||
authorize: "Autorizo",
|
||||
deny: "Refuzo",
|
||||
denyNotePlaceholder: "arsyeja e refuzimit (opsionale)",
|
||||
},
|
||||
profile: {
|
||||
title: "Profili im",
|
||||
accountSection: "Llogaria",
|
||||
@@ -187,6 +217,8 @@ export const sq = {
|
||||
evtCashMovement: "ARKË",
|
||||
evtCashIn: "ARKËTIM",
|
||||
evtCashOut: "PAGESË",
|
||||
evtCashReview: "SHQYRTIM",
|
||||
decision: { authorize: "autorizuar", deny: "refuzuar" },
|
||||
evtAnomaly: "ANOMALI",
|
||||
evtRefused: "REFUZUAR",
|
||||
// rreshti i detajeve të eventit live + etiketat e klasifikimit (nga payload)
|
||||
@@ -228,6 +260,10 @@ export const sq = {
|
||||
edPlate: "Targa",
|
||||
edCategory: "Kategoria",
|
||||
edOperator: "Operatori",
|
||||
edDecision: "Vendimi i shqyrtimit",
|
||||
edReviewedBy: "Shqyrtuar nga",
|
||||
edReviewNote: "Shënim",
|
||||
edReviewRef: "Ref. lëvizjes",
|
||||
edTariffVersion: "Versioni i tarifës",
|
||||
edRawPayload: "Të dhënat e papërpunuara të nënshkruara",
|
||||
edOccurrence: "ID e hyrjes",
|
||||
@@ -722,9 +758,9 @@ export const sq = {
|
||||
srcSubWindow: "jashtë orarit",
|
||||
drawerSection: "— Arka —",
|
||||
openingFloat: "Arka fillestare:",
|
||||
cashTaken: "Para të marra:",
|
||||
cashAdded: "Para të shtuara:",
|
||||
cashRemoved: "Para të hequra:",
|
||||
cashTaken: "Xhiro ditore:",
|
||||
cashAdded: "Arkëtime:",
|
||||
cashRemoved: "Pagesa:",
|
||||
expectedDrawer: "Gjëndje Arke:",
|
||||
printedToReceipt: "Printuar te printeri i kabinës.",
|
||||
recordedNoPrinter: "Regjistruar (pa printer për të printuar).",
|
||||
@@ -775,9 +811,9 @@ export const sq = {
|
||||
// Expanded drawer detail.
|
||||
drawerSection: "Arka",
|
||||
openingFloat: "Arka fillestare",
|
||||
cashTaken: "Para të marra",
|
||||
cashAdded: "Para të shtuara",
|
||||
cashRemoved: "Para të hequra",
|
||||
cashTaken: "Xhiro ditore",
|
||||
cashAdded: "Arkëtime",
|
||||
cashRemoved: "Pagesa",
|
||||
loadFailed: "Ngarkimi i turneve dështoi.",
|
||||
},
|
||||
reports: {
|
||||
|
||||
Reference in New Issue
Block a user