diff --git a/frontend/components/pharmacy/add-inventory-dialog.tsx b/frontend/components/pharmacy/add-inventory-dialog.tsx index 44feeee..c06f433 100644 --- a/frontend/components/pharmacy/add-inventory-dialog.tsx +++ b/frontend/components/pharmacy/add-inventory-dialog.tsx @@ -1,10 +1,14 @@ "use client"; -import { ScanLine } from "lucide-react"; -import { type FormEvent, type ReactNode, useState } from "react"; +import { + type FormEvent, + type ReactNode, + useCallback, + useEffect, + useState, +} from "react"; import { useTranslation } from "react-i18next"; -import { BarcodeScanner } from "@/components/scan/barcode-scanner"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -56,7 +60,6 @@ export function AddInventoryDialog({ // Lot/batch parsed from a GS1 scan. No dedicated field, so it rides along in // notes (surfaced on the item detail). const [notes, setNotes] = useState(""); - const [scannerOpen, setScannerOpen] = useState(false); const reset = () => { setName(""); @@ -73,18 +76,68 @@ export function AddInventoryDialog({ // A scanned medication barcode: store the code, and when it's a GS1 // DataMatrix, auto-fill expiry (AI 17) and lot (AI 10, -> notes). - const handleScan = (raw: string) => { - setScannerOpen(false); - const gs1 = parseGs1(raw); - if (gs1) { - setBarcode(gs1.gtin ?? raw); - if (gs1.expiry) setExpiresAt(gs1.expiry); - if (gs1.lot) setNotes((n) => n || `Lot: ${gs1.lot}`); - } else { - setBarcode(raw.trim()); - } - notify.success(t("inventory.dialog.scannedTitle"), gs1?.gtin ?? raw.trim()); - }; + const handleScan = useCallback( + (raw: string) => { + const gs1 = parseGs1(raw); + if (gs1) { + setBarcode(gs1.gtin ?? raw); + if (gs1.expiry) setExpiresAt(gs1.expiry); + if (gs1.lot) setNotes((n) => n || `Lot: ${gs1.lot}`); + } else { + setBarcode(raw.trim()); + } + notify.success( + t("inventory.dialog.scannedTitle"), + gs1?.gtin ?? raw.trim(), + ); + }, + [t], + ); + + // Hardware barcode scanners are keyboard wedges: they "type" the code as a + // fast burst of keystrokes terminated by Enter. While the dialog is open we + // watch keydowns globally and, when a fast burst ends in Enter, treat the + // buffer as a scan — so the pharmacist can just scan a medication without + // first focusing any field. The timing gate (keys < 50ms apart) keeps + // ordinary typing out of the buffer; once a burst is detected we swallow the + // characters so they don't leak into the focused input. + useEffect(() => { + if (!open) return; + let buffer = ""; + let lastTime = 0; + const MAX_GAP_MS = 50; + const MIN_LEN = 6; + + const onKeyDown = (event: KeyboardEvent) => { + const now = Date.now(); + const fast = now - lastTime <= MAX_GAP_MS; + lastTime = now; + + if (event.key === "Enter") { + if (fast && buffer.length >= MIN_LEN) { + event.preventDefault(); + event.stopPropagation(); + handleScan(buffer); + } + buffer = ""; + return; + } + if ( + event.key.length === 1 && + !event.metaKey && + !event.ctrlKey && + !event.altKey + ) { + buffer = fast ? buffer + event.key : event.key; + if (fast && buffer.length >= 2) event.preventDefault(); + } else { + buffer = ""; + } + }; + + window.addEventListener("keydown", onKeyDown, true); + return () => window.removeEventListener("keydown", onKeyDown, true); + }, [open, handleScan]); const submit = (event: FormEvent) => { event.preventDefault(); @@ -114,7 +167,6 @@ export function AddInventoryDialog({ }; return ( - <> { onOpenChange(o); @@ -204,23 +256,24 @@ export function AddInventoryDialog({ -
- setBarcode(event.target.value)} - placeholder={t("inventory.dialog.barcodePlaceholder")} - value={barcode} - /> - -
+ setBarcode(event.target.value)} + // A hardware scanner sends Enter after the code; parse it here + // (instead of submitting the form) so a scan into this field is + // handled like the global wedge burst. + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (barcode.trim()) handleScan(barcode); + } + }} + placeholder={t("inventory.dialog.barcodePlaceholder")} + value={barcode} + /> + + {t("inventory.dialog.scanHint")} +
@@ -235,11 +288,5 @@ export function AddInventoryDialog({
- - ); } diff --git a/frontend/lib/i18n/locales/ar/translation.json b/frontend/lib/i18n/locales/ar/translation.json index c25efcb..a9bf468 100644 --- a/frontend/lib/i18n/locales/ar/translation.json +++ b/frontend/lib/i18n/locales/ar/translation.json @@ -713,7 +713,7 @@ "expires": "تنتهي الصلاحية", "barcode": "الباركود / NDC", "barcodePlaceholder": "امسح أو اكتب رمزًا", - "scan": "مسح الباركود", + "scanHint": "وصّل ماسح باركود USB وامسح الدواء — أو اكتب الرمز.", "scannedTitle": "تم مسح الباركود", "cancel": "إلغاء", "submit": "إضافة عنصر", diff --git a/frontend/lib/i18n/locales/de/translation.json b/frontend/lib/i18n/locales/de/translation.json index 6b1ca81..57f7574 100644 --- a/frontend/lib/i18n/locales/de/translation.json +++ b/frontend/lib/i18n/locales/de/translation.json @@ -697,7 +697,7 @@ "expires": "Verfällt", "barcode": "Barcode / NDC", "barcodePlaceholder": "Code scannen oder eingeben", - "scan": "Barcode scannen", + "scanHint": "Schließen Sie einen USB-Barcode-Scanner an und scannen Sie das Medikament – oder geben Sie den Code ein.", "scannedTitle": "Barcode gescannt", "cancel": "Abbrechen", "submit": "Artikel hinzufügen", diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index b249c82..0628132 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -697,7 +697,7 @@ "expires": "Expires", "barcode": "Barcode / NDC", "barcodePlaceholder": "Scan or type a code", - "scan": "Scan barcode", + "scanHint": "Connect a USB barcode scanner and scan the medication — or type the code.", "scannedTitle": "Barcode scanned", "cancel": "Cancel", "submit": "Add item", diff --git a/frontend/lib/i18n/locales/fr/translation.json b/frontend/lib/i18n/locales/fr/translation.json index 7316aaf..fa2cc45 100644 --- a/frontend/lib/i18n/locales/fr/translation.json +++ b/frontend/lib/i18n/locales/fr/translation.json @@ -697,7 +697,7 @@ "expires": "Expire le", "barcode": "Code-barres / NDC", "barcodePlaceholder": "Scanner ou saisir un code", - "scan": "Scanner le code-barres", + "scanHint": "Branchez un lecteur de code-barres USB et scannez le médicament — ou saisissez le code.", "scannedTitle": "Code-barres scanné", "cancel": "Annuler", "submit": "Ajouter l'article", diff --git a/frontend/lib/i18n/locales/so/translation.json b/frontend/lib/i18n/locales/so/translation.json index a2cf8bb..a647fcf 100644 --- a/frontend/lib/i18n/locales/so/translation.json +++ b/frontend/lib/i18n/locales/so/translation.json @@ -697,7 +697,7 @@ "expires": "Dhacaya", "barcode": "Barcode / NDC", "barcodePlaceholder": "Iskaan garee ama qor koodh", - "scan": "Iskaan garee barcode", + "scanHint": "Ku xir iskaanka barcode-ka USB oo iskaan garee daawada — ama qor koodhka.", "scannedTitle": "Barcode la iskaan gareeyay", "cancel": "Jooji", "submit": "Ku dar shay",