import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
fetchDrawerBalance,
fetchDrawerMovements,
fetchEvents,
fetchShift,
fetchShiftReport,
fetchShifts,
recordDrawerMovement,
reviewDrawerMovement,
type DrawerMovement,
type MovementStatus,
type ShiftSummary,
type TillId,
} from "./api.js";
import { formatClock, formatMoney, formatRelativeDateTime } from "./lib/format.js";
import { shiftKey } from "./lib/use-shift.js";
import { Panel } from "./ui/Panel.js";
import { tillOf, type LedgerEvent } from "@parking/shared";
// The DRAWER HUB (redesigned 2026-07-05 — was only record + review). One screen
// answers "what's in the till and why": the CURRENT drawer balance with the open
// shift's running breakdown (float + takings + vouchers = expected), TODAY's cash
// activity (every cash payment and voucher, live), the movement record/review flow
// (unchanged), and the closed-shift drawer history. All figures come from the signed
// chain. TILLS (2026-09-05): there is one drawer PER TILL (booth, wash desk); the hub
// shows one till at a time — a switch appears when the site has more than one — and
// every panel below is scoped to it. See wiki/concepts/shift.md "Tills".
const money = (m: number, cur: string | null) => formatMoney(m, cur ?? "");
/** Local midnight, ISO — the "today" window for the activity feed. */
function startOfToday(): string {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d.toISOString();
}
function StatusBadge({ status }: { status: MovementStatus }) {
const { t } = useTranslation();
const cls =
status === "authorized"
? "border-term-green/60 text-term-green"
: status === "denied"
? "border-term-red/60 text-term-red"
: "border-term-amber/60 text-term-amber";
return (
{t(`drawer.status.${status}`)}
);
}
export function DrawerManager({ canCreate, canReview }: { canCreate: boolean; canReview: boolean }) {
const { t } = useTranslation();
const qc = useQueryClient();
const [till, setTill] = useState("booth");
// Which tills exist here (the booth + effective money-taking modules') — from the
// booth's status read, which every till answer carries.
const status = useQuery({ queryKey: shiftKey("booth"), queryFn: () => fetchShift("booth") });
const tills = status.data?.tills ?? ["booth"];
const refresh = () => {
void qc.invalidateQueries({ queryKey: ["drawer"] });
// A voucher moves the open shift's added/removed figures too (the X-report).
void qc.invalidateQueries({ queryKey: ["shift"] });
};
return (
{/* Till switch — only when there is more than one drawer to look at. */}
{tills.length > 1 && (
{tills.map((x) => (
))}
)}
{/* Row 1: the till NOW + the record form. */}
{canCreate && }
{/* Row 2: today's cash feed · the movement review queue · closed shifts. */}
);
}
// --- The drawer NOW ---------------------------------------------------------
// Balance from the chain + the open shift's running X-report breakdown, so the big
// number is always explainable: float + cash takings + in − out = expected = balance.
function StatePanel({ till }: { till: TillId }) {
const { t } = useTranslation();
const balance = useQuery({ queryKey: ["drawer", "balance", till], queryFn: () => fetchDrawerBalance(till), refetchInterval: 10_000 });
const status = useQuery({ queryKey: shiftKey(till), queryFn: () => fetchShift(till) });
const report = useQuery({
queryKey: ["shift", "xreport", till],
queryFn: () => fetchShiftReport(till),
enabled: status.data?.open != null,
refetchInterval: 10_000,
});
const x = status.data?.open ? report.data : null;
const cur = balance.data?.currency ?? x?.currency ?? null;
// The current SHIFT's own balance: what this shift changed in the till
// (takings + vouchers), i.e. everything above the inherited opening float.
const shiftDelta = x ? x.expectedDrawerMinor - x.openingFloatMinor : null;
return (