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:
@@ -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