feat(booth): open the pay/exit modal on a hardware scan regardless of focus
A barcode/QR scanner is an HID "keyboard wedge" — it types the id + Enter into whatever holds focus. Previously that only worked while the ticket <input> was focused; a scan with focus elsewhere (or nowhere) went nowhere. New useScanner hook (apps/web/src/lib/use-scanner.ts): a document-level keydown listener that detects the scanner's FAST keystroke burst ended by Enter and opens the pay/exit modal via setActiveTicket — regardless of focus. A gap > 50ms resets the buffer, so human-paced typing with nothing focused never registers as a scan (min length 3 guards stray Enters). Keystrokes into an input/textarea/select/ contenteditable are ignored, so the manual ticket field still works by hand. The hook is paused while a modal is already open — a scan must not abandon an in-progress payment; the operator finishes/closes, then scans the next car. Verified at runtime (Playwright): a fast burst with focus on BODY opens the modal; a second scan while the modal is open is ignored; slow (120ms) human typing does NOT open it; the manual input submit still opens it. build+lint 14/14. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -5,6 +5,7 @@ import { fetchEvents, fetchOccupancy, type LedgerEvent, type Occupancy } from ".
|
||||
import { qk } from "./lib/query.js";
|
||||
import { useLiveStore } from "./lib/live-store.js";
|
||||
import { useShift } from "./lib/use-shift.js";
|
||||
import { useScanner } from "./lib/use-scanner.js";
|
||||
import { Panel } from "./ui/Panel.js";
|
||||
import { StatusDot } from "./ui/StatusDot.js";
|
||||
import { BoothPayModal } from "./BoothPayModal.js";
|
||||
@@ -132,6 +133,11 @@ export function BoothScreen() {
|
||||
// The ledger event open in the read-only detail modal (null = closed).
|
||||
const [detailEvent, setDetailEvent] = useState<LedgerEvent | null>(null);
|
||||
|
||||
// A hardware scan opens the pay/exit modal regardless of focus (the operator needn't
|
||||
// click the ticket field first). Paused while a modal is already up — a scan must not
|
||||
// abandon an in-progress payment (the operator finishes/closes, then scans the next).
|
||||
useScanner({ onScan: setActiveTicket, paused: activeTicket != null || detailEvent != null });
|
||||
|
||||
// Live-feed filters: free-text search, event category, and direction/source.
|
||||
const [feedSearch, setFeedSearch] = useState("");
|
||||
const [feedType, setFeedType] = useState<FeedCat | "">("");
|
||||
|
||||
Reference in New Issue
Block a user