feat(drawer): drawer hub — balance now, this-shift figure, daily activity, shift history; busy spinners
Build desktop / desktop (push) Successful in 4m12s
Build & push images / images (push) Successful in 2m53s
CI / check (push) Successful in 41s

/drawer was record + review only: no current balance, no sight of the open
shift's incomings, no daily activity, no shift history. Rebuilt as a hub:

- Drawer now: the till's running balance (new GET /api/drawer/balance,
  shift:read — exposes the service's existing drawerBalance(); the drawer
  is one site-wide till, same exposure the X-report already had) with the
  open shift's X-report breakdown alongside (float + takings + vouchers =
  expected = balance) and a "This shift: ±X" figure (expected − opening
  float — the shift's own contribution vs what it inherited).
- Today's cash activity: every cash payment + voucher since local
  midnight from the signed chain, live, with day totals (card never
  enters the till).
- Record + movements/review: the 2026-07-01 flow, unchanged.
- Closed shifts: drawer-focused history via the scope-aware /api/shifts
  (float → takings ± vouchers → expected per shift).

Also: every shift open/close button (header, /shifts, pay modal, end-
shift confirm) now shows an animated spinner + dims while busy — the old
label-swap-only feedback read as a dead click when a shift open ran slow.
The slowness itself (drawer/shift reads fold the WHOLE chain, O(chain))
is recorded as an open item in wiki/concepts/shift.md with the fix
sketch: fold from the last z-report's signed expectedDrawerMinor forward.

No new ledger surface — one read-only endpoint; RBAC test added.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-05 15:56:30 +02:00
parent d5ff2097bd
commit c5ed3f1308
13 changed files with 489 additions and 72 deletions
+9 -2
View File
@@ -15,6 +15,7 @@ import {
import { formatMoney, formatDuration, formatRelativeDateTime } from "./lib/format.js";
import { CARD_PAYMENTS_ENABLED } from "./lib/features.js";
import { Modal } from "./ui/Modal.js";
import { Spinner } from "./ui/Spinner.js";
import { EventDetailModal, EventRow } from "./ui/event-detail.js";
import type { LedgerEvent } from "@parking/shared";
@@ -176,7 +177,7 @@ export function ShiftsHistory({ user, canManage = false }: { user: SessionUser |
)}
{isAdmin && (
<div className="field">
<span className="label">{t("shifts.operator")}</span>
{/* <span className="label">{t("shifts.operator")}</span> */}
{/* A select over operators that HAVE shifts — the server filter is an
exact username match, so free text could only miss. */}
<select className="input w-44" value={operator} onChange={(e) => setOperator(e.target.value)}>
@@ -244,7 +245,13 @@ function StartShiftButton({ onDone }: { onDone: () => void }) {
<span className="flex items-center gap-2">
{err && <span className="text-[0.75rem] text-term-red">{err}</span>}
<button type="button" className="btn btn-go btn-sm" onClick={start} disabled={busy}>
{busy ? t("shift.starting") : t("shift.startShift")}
{busy ? (
<span className="inline-flex items-center gap-1.5">
<Spinner /> {t("shift.starting")}
</span>
) : (
t("shift.startShift")
)}
</button>
</span>
);