import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { closeShift, fetchShift, openShift, recordCashMovement, type ShiftReport } from "./api.js"; // Manned-mode shift control. Start/End are explicit (not time-based — see // wiki/concepts/shift.md). End Shift signs + prints a Z-report and shows the // totals + the DRAWER picture (opening float carried from the prior shift, cash // taken/added/removed, expected drawer). Admins can load/remove drawer cash. // Available to cashier/operator/admin (readonly has no shift). const money = (m: number, cur: string | null) => `${(m / 100).toFixed(2)} ${cur ?? ""}`.trim(); export function ShiftControl({ isAdmin = false }: { isAdmin?: boolean }) { const { t } = useTranslation(); const [startedAt, setStartedAt] = useState(null); const [drawerMinor, setDrawerMinor] = useState(null); const [currency, setCurrency] = useState(null); const [busy, setBusy] = useState(false); const [report, setReport] = useState(null); const [err, setErr] = useState(null); // Cash-movement form (admin only). const [moveAmount, setMoveAmount] = useState(""); const [moveReason, setMoveReason] = useState(""); const [moveMsg, setMoveMsg] = useState(null); function refresh() { fetchShift() .then((s) => { setStartedAt(s.open?.startedAt ?? null); setDrawerMinor(s.drawerMinor); setCurrency(s.currency); }) .catch(() => { /* readonly / not permitted — hide control */ }); } useEffect(refresh, []); async function start() { setBusy(true); setErr(null); setReport(null); try { const { startedAt } = await openShift(); setStartedAt(startedAt); refresh(); } catch (e) { setErr((e as Error).message); } finally { setBusy(false); } } async function end() { setBusy(true); setErr(null); try { const z = await closeShift(); setReport(z); setStartedAt(null); refresh(); } catch (e) { setErr((e as Error).message); } finally { setBusy(false); } } async function move(sign: 1 | -1) { setMoveMsg(null); const major = Number(moveAmount); if (!Number.isFinite(major) || major <= 0) { setMoveMsg(t("shift.enterPositive")); return; } try { const r = await recordCashMovement(sign * Math.round(major * 100), moveReason.trim()); setMoveAmount(""); setMoveReason(""); setMoveMsg(t("shift.drawerNow", { amount: money(r.balanceMinor, currency) })); refresh(); } catch (e) { setMoveMsg((e as Error).message); } } return (
{t("shift.label")}{" "} {startedAt ? ( <> {t("shift.open")} {t("shift.since")}{" "} {new Date(startedAt).toLocaleString()}{" "} ) : ( <> {t("shift.notStarted")}{" "} )} {/* Live drawer balance (what's in the till right now / inherited). */} {drawerMinor != null && (
{t("shift.drawer")} {money(drawerMinor, currency)} {startedAt && {t("shift.openingFloatInherited")}}
)} {err &&

{err}

} {/* Admin: load / remove physical drawer cash (signed cash_movement). */} {isAdmin && (
{t("shift.drawerCashAdmin")}
setMoveAmount(e.target.value)} placeholder={t("shift.amount")} inputMode="decimal" style={{ width: 90 }} /> setMoveReason(e.target.value)} placeholder={t("shift.reasonPlaceholder")} style={{ flex: 1, minWidth: 140 }} />
{moveMsg &&
{moveMsg}
}
)} {report && (
{t("shift.zReport")} — {report.operator}
{t("shift.payments")} {report.paymentCount}
{t("shift.cash")} {money(report.cashTotalMinor, report.currency)}
{t("shift.card")} {money(report.cardTotalMinor, report.currency)}
{t("shift.drawerSection")}
{t("shift.openingFloat")} {money(report.openingFloatMinor, report.currency)}
{t("shift.cashTaken")} {money(report.cashTotalMinor, report.currency)}
{t("shift.cashAdded")} {money(report.cashAddedMinor, report.currency)}
{t("shift.cashRemoved")} {money(report.cashRemovedMinor, report.currency)}
{t("shift.expectedDrawer")} {money(report.expectedDrawerMinor, report.currency)}
{report.printed ? t("shift.printedToReceipt") : t("shift.recordedNoPrinter")}
)}
); }