import { useState } from "react"; import { useTranslation } from "react-i18next"; import * as Dialog from "@radix-ui/react-dialog"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { boothExit, fetchSiteConfig, lookupSession, paySession, printVoucher, type SessionLookup, } from "./api.js"; import { qk } from "./lib/query.js"; import { formatDuration, formatMoney, formatTime } from "./lib/format.js"; import { SnapshotStrip } from "./ui/SnapshotStrip.js"; // The booth pay/exit modal. Opened when the operator submits a ticket id. Shows the // session (entry, exit=now, duration, total owed) + entry/exit snapshots, takes // payment, then EITHER prints an exit voucher (customer self-exits at a distant // exit) OR fires the exit immediately (booth at/near the exit) — controlled by a // checkbox defaulting from site_config.exitVoucherDefault. See booth-exit-flow.md. type Phase = "review" | "paying" | "finishing" | "done" | "error"; export function BoothPayModal({ identity, onClose }: { identity: string; onClose: () => void }) { const { t } = useTranslation(); const qc = useQueryClient(); const session = useQuery({ queryKey: ["session", identity], queryFn: () => lookupSession(identity) }); const config = useQuery({ queryKey: qk.siteConfig, queryFn: fetchSiteConfig }); const [tender, setTender] = useState<"cash" | "card">("cash"); const [printVoucherChecked, setPrintVoucherChecked] = useState(null); const [phase, setPhase] = useState("review"); const [error, setError] = useState(null); const [result, setResult] = useState(null); const s: SessionLookup | undefined = session.data; // Checkbox default comes from config the first time it loads; operator can toggle. const voucher = printVoucherChecked ?? config.data?.exitVoucherDefault ?? false; const alreadyPaid = s?.paidAt != null; const canPay = s?.found && s.open && !alreadyPaid; async function handlePayAndExit() { if (!s) return; setError(null); try { // 1. Take payment (unless already paid — e.g. paid earlier at a kiosk). if (!alreadyPaid) { setPhase("paying"); await paySession(identity, tender); } // 2. Voucher OR immediate exit. setPhase("finishing"); if (voucher) { const r = await printVoucher(identity); setResult(t("pay.voucherPrinted", { printer: r.printedBy })); } else { const r = await boothExit(identity); setResult( r.opened ? t("pay.paidBarrierOpened") : t("pay.paidExitRecorded", { reason: r.reason ?? t("booth.openManually") }), ); } // Refresh the live views. void qc.invalidateQueries({ queryKey: qk.events }); void qc.invalidateQueries({ queryKey: qk.occupancy }); setPhase("done"); } catch (e) { setError((e as Error).message); setPhase("error"); } } return ( !o && onClose()}>
{t("pay.ticket")} {identity} ✕
{session.isLoading &&
{t("pay.lookingUp")}
} {s && !s.found && (
{t("pay.noSessionFound")}
)} {s && s.found && !s.open && (
{t("pay.alreadyClosed", { time: formatTime(s.exitedAt) })}
)} {s && s.found && s.open && ( <> {/* Session figures */}
{/* Total */}
{t("pay.total")} {s.amountMinor != null && s.currency ? formatMoney(s.amountMinor, s.currency) : alreadyPaid ? t("booth.badgePaid") : t("pay.noTariff")}
{/* Snapshots */} {phase !== "done" && ( <> {/* Tender */} {canPay && (
{t("pay.tender")} {(["cash", "card"] as const).map((tn) => ( ))}
)} {/* Voucher checkbox (default from site config) */} )} {error &&
{error}
} {result && (
{result}
)} {/* Actions */}
{phase === "done" ? ( ) : ( <> )}
)}
); } function Row({ label, value, valueClass = "" }: { label: string; value: string; valueClass?: string }) { return (
{label} {value}
); }