From 3ed785c33ee8cd509a46e546dfce67177b1148b9 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sun, 21 Jun 2026 15:03:01 +0200 Subject: [PATCH] feat(booth): open the pay/exit modal on a hardware scan regardless of focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- apps/web/src/BoothScreen.tsx | 6 +++ apps/web/src/lib/use-scanner.ts | 78 +++++++++++++++++++++++++++++++++ wiki/concepts/booth-console.md | 8 ++++ wiki/log.md | 10 +++++ 4 files changed, 102 insertions(+) create mode 100644 apps/web/src/lib/use-scanner.ts diff --git a/apps/web/src/BoothScreen.tsx b/apps/web/src/BoothScreen.tsx index 7fa2a2f..e8f9314 100644 --- a/apps/web/src/BoothScreen.tsx +++ b/apps/web/src/BoothScreen.tsx @@ -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(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(""); diff --git a/apps/web/src/lib/use-scanner.ts b/apps/web/src/lib/use-scanner.ts new file mode 100644 index 0000000..0c931b9 --- /dev/null +++ b/apps/web/src/lib/use-scanner.ts @@ -0,0 +1,78 @@ +import { useEffect, useRef } from "react"; + +// Global hardware-scanner capture (HID "keyboard wedge"). A barcode/QR scanner types +// the code as a fast keystroke burst followed by Enter — like a keyboard, but far +// faster than a human. This hook listens at the DOCUMENT level so a scan fires the +// callback no matter what's focused (or if nothing is), unlike a single that +// only catches scans while it holds focus. See wiki/concepts/booth-console.md. +// +// It does NOT hijack manual typing: keystrokes into an /