import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { fetchShifts, type ShiftSummary, type SessionUser } from "./api.js"; import { formatMoney, formatDuration, formatRelativeDateTime } from "./lib/format.js"; // Completed shift history. Scope is enforced SERVER-SIDE by permission: an operator // gets only their own shifts; an admin (shift:cash) gets all + a date/operator // filter. The screen mirrors that — it shows the filter only when the server // reports scope:"all". Each row is one signed shift_z_report; expanding it shows the // drawer reconciliation. See wiki/concepts/shift.md. function money(minor: number, currency: string | null): string { return currency ? formatMoney(minor, currency) : (minor / 100).toFixed(2); } export function ShiftsHistory({ user }: { user: SessionUser | null }) { const { t } = useTranslation(); // Admin filter inputs (only sent when the server grants the "all" scope; for an // operator the server ignores them anyway). const [operator, setOperator] = useState(""); const [from, setFrom] = useState(""); const [to, setTo] = useState(""); // The applied filter (separate from the inputs, so typing doesn't refetch). const [applied, setApplied] = useState<{ operator?: string; from?: string; to?: string }>({}); const q = useQuery({ queryKey: ["shifts", applied], queryFn: () => fetchShifts(applied), }); const isAdmin = q.data?.scope === "all"; const shifts = q.data?.shifts ?? []; function apply() { setApplied({ operator: operator.trim() || undefined, // A date input gives yyyy-mm-dd; widen `to` to the end of that day. from: from ? new Date(`${from}T00:00:00`).toISOString() : undefined, to: to ? new Date(`${to}T23:59:59`).toISOString() : undefined, }); } function clear() { setOperator(""); setFrom(""); setTo(""); setApplied({}); } return (

{isAdmin ? t("shifts.title") : t("shifts.myTitle")}

{/* Admin-only filter: by operator + a date window over the shift start. */} {isAdmin && (
{t("shifts.operator")} setOperator(e.target.value)} placeholder={t("shifts.allOperators")} />
{t("shifts.filterFrom")} setFrom(e.target.value)} />
{t("shifts.filterTo")} setTo(e.target.value)} />
)} {q.isError && (
{t("shifts.loadFailed")}
)}
{isAdmin && } {shifts.map((s) => ( ))} {!q.isLoading && shifts.length === 0 && ( )}
{t("shifts.operator")}{t("shifts.started")} {t("shifts.ended")} {t("shifts.payments")} {t("shifts.cash")} {t("shifts.card")} {t("shifts.expectedDrawer")}
{t("shifts.none")}
); } function ShiftRow({ s, showOperator, colSpan }: { s: ShiftSummary; showOperator: boolean; colSpan: number }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const cur = s.currency; const when = (iso: string) => formatRelativeDateTime(iso, t); return ( <> setOpen((o) => !o)} > {showOperator && {s.operator}} {when(s.startedAt)} {when(s.endedAt)} {formatDuration(s.startedAt, s.endedAt)} {s.paymentCount} {money(s.cashTotalMinor, cur)} {money(s.cardTotalMinor, cur)} {money(s.expectedDrawerMinor, cur)} {open && (
{t("shifts.drawerSection")}
)} ); } function Figure({ label, value }: { label: string; value: string }) { return (
{label} {value}
); }