'use client'; import { useEffect, useState, type SyntheticEvent } from 'react'; import clsx from 'clsx'; import { CheckCircleIcon, XCircleIcon, BanknotesIcon } from '@heroicons/react/24/outline'; import type { SessionEntry } from './SessionSheet'; // Full-screen outcome after a door action. At a loud, dark door a row flash and // a small toast are too easy to miss; a screen that is entirely green or // entirely red answers "is this person in or not?" from across the table. // // It never traps the queue: tapping anywhere closes it, and it closes itself // after a few seconds. The undo affordance survives the close — the page shows // the bottom Undo toast for whatever is left of the window. export type DoorResultKind = 'success' | 'already_in' | 'not_found' | 'unpaid' | 'failed'; export interface DoorResult { kind: DoorResultKind; /** Headline: the attendee's name, or a short verdict when there is no one. */ name: string; /** Second line: "Checked in 19:42 · paid cash", "Already in at 19:42", "Collect ₲60.000". */ detail?: string; /** Success only — drives Undo and the post-close toast. */ entry?: SessionEntry; } // Success needs no reading; the red ones carry a time or an amount the staff // member has to relay to the person, so they get a little longer. export const RESULT_AUTO_CLOSE_MS: Record = { success: 4000, already_in: 6000, not_found: 6000, unpaid: 6000, failed: 6000, }; const SURFACE: Record = { success: { bg: 'bg-emerald-600', text: 'text-emerald-700', icon: CheckCircleIcon, title: 'Checked in' }, already_in: { bg: 'bg-red-600', text: 'text-red-700', icon: XCircleIcon, title: 'Already checked in' }, not_found: { bg: 'bg-red-600', text: 'text-red-700', icon: XCircleIcon, title: 'Not found' }, unpaid: { bg: 'bg-amber-600', text: 'text-amber-700', icon: BanknotesIcon, title: 'Payment due' }, failed: { bg: 'bg-red-700', text: 'text-red-800', icon: XCircleIcon, title: 'NOT checked in' }, }; export function ResultScreen({ result, onClose, onUndo, }: { result: DoorResult; onClose: () => void; onUndo: () => void; }) { const surface = SURFACE[result.kind]; const duration = RESULT_AUTO_CLOSE_MS[result.kind]; // Toggled after mount so the fade-in and the countdown bar both animate from // their starting state instead of appearing already finished. const [shown, setShown] = useState(false); // Keyed on the result identity: a success that flips to failed restarts both // the fade and the countdown for the new state. const identity = `${result.kind}:${result.entry?.idempotencyKey ?? result.name}`; useEffect(() => { setShown(false); const raf = requestAnimationFrame(() => setShown(true)); const timer = setTimeout(onClose, duration); return () => { cancelAnimationFrame(raf); clearTimeout(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [identity]); const stop = (e: SyntheticEvent) => e.stopPropagation(); const Icon = surface.icon; return (

{surface.title}

{result.name}

{result.detail &&

{result.detail}

}

Tap anywhere to continue

{result.kind === 'success' && ( )}
{/* Countdown to auto-close, so the screen vanishing never surprises anyone. */}
); }