"use client"; import { QrCodeInfo } from "@/app/api/qrcode"; import { useRef } from "react"; import { ReactNode, createContext, useCallback, useContext, useEffect, useState, startTransition, } from "react"; export interface QrCodeScanner { values: string[]; isScanning: boolean; startScan: () => void; stopScan: () => void; resetScan: () => void; result: QrCodeInfo | undefined; state: "scanning" | "pending" | "retry"; error: string | undefined; } interface QrCodeScannerProviderProps { children: ReactNode; } export const QrCodeScannerContext = createContext( undefined, ); const QrCodeScannerProvider: React.FC = ({ children, }) => { const [qrCodeScannerValues, setQrCodeScannerValues] = useState([]); const [isScanning, setIsScanning] = useState(false); const [keys, setKeys] = useState([]); const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState(0); const [rightCurlyBraceCount, setRightCurlyBraceCount] = useState(0); const [scanResult, setScanResult] = useState() const [scanState, setScanState] = useState<"scanning" | "pending" | "retry">("pending"); const [scanError, setScanError] = useState() // TODO return scan error message const keysRef = useRef([]); const leftBraceCountRef = useRef(0); const rightBraceCountRef = useRef(0); const isFirstKeyRef = useRef(true); const resetScannerInput = useCallback(() => { setKeys(() => []); setLeftCurlyBraceCount(() => 0); setRightCurlyBraceCount(() => 0); }, []); const resetQrCodeScanner = useCallback((error : string = "") => { setQrCodeScannerValues(() => []); setScanResult(undefined); resetScannerInput(); console.log("%c Scanner Reset", "color:cyan"); if (error.length > 0) { console.log("%c Error:", "color:red", error); console.log("%c key:", "color:red", keys); setScanState("retry"); } }, []); const startQrCodeScanner = useCallback(() => { const startTime = performance.now(); console.log(`⏱️ [SCANNER START] Called at: ${new Date().toISOString()}`); resetQrCodeScanner(); const resetTime = performance.now() - startTime; console.log(`⏱️ [SCANNER START] Reset time: ${resetTime.toFixed(2)}ms`); setIsScanning(() => true); const setScanningTime = performance.now() - startTime; console.log(`⏱️ [SCANNER START] setScanning time: ${setScanningTime.toFixed(2)}ms`); const totalTime = performance.now() - startTime; console.log(`%c Scanning started `, "color:cyan"); console.log(`⏱️ [SCANNER START] Total start time: ${totalTime.toFixed(2)}ms`); console.log(`⏰ [SCANNER START] Scanner started at: ${new Date().toISOString()}`); }, [resetQrCodeScanner]); const endQrCodeScanner = useCallback(() => { setIsScanning(() => false); console.log("%c Scanning stopped ", "color:cyan"); }, []); // Find by rough match, return 0 if not found const findIdByRoughMatch = (inputString : string, keyword : string) => { console.log(`%c Performed rough match for ${keyword} within ${inputString}`, "color:brown"); const keywordIndex = inputString.indexOf(keyword); let result : {keywordFound: boolean; number: number | null; message: string} = { keywordFound: false, number: null, message: `${keyword} not found in the input`, }; if (keywordIndex !== -1) { const substringAfterKeyword = inputString.slice(keywordIndex + keyword.length); const numberMatch = substringAfterKeyword.match(/\d+/); if (!numberMatch) { result = { keywordFound: true, number: null, message: `No valid number found after ${keyword}`, }; } else { result = { keywordFound: true, number: parseInt(numberMatch[0], 10), message: `Found ${keyword} at index ${keywordIndex}, first number found after is: ${numberMatch[0]}`, }; } } console.log(`%c ${result.message}`, "color:brown"); return result; }; useEffect(() => { const effectStartTime = performance.now(); console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Triggered at: ${new Date().toISOString()}`); console.log(`⏱️ [KEYBOARD LISTENER EFFECT] isScanning: ${isScanning}`); if (isScanning) { const listenerRegisterStartTime = performance.now(); console.log(`⏱️ [KEYBOARD LISTENER] Registering keyboard listener at: ${new Date().toISOString()}`); // Reset refs when starting scan keysRef.current = []; leftBraceCountRef.current = 0; rightBraceCountRef.current = 0; isFirstKeyRef.current = true; const handleKeyDown = (event: KeyboardEvent) => { const keyPressTime = performance.now(); const keyPressTimestamp = new Date().toISOString(); // ✅ OPTIMIZED: Use refs to accumulate keys immediately (no state update delay) if (event.key.length === 1) { if (isFirstKeyRef.current) { console.log(`⏱️ [KEYBOARD] First key press detected: "${event.key}"`); console.log(`⏰ [KEYBOARD] First key press at: ${keyPressTimestamp}`); console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(keyPressTime - listenerRegisterStartTime).toFixed(2)}ms`); isFirstKeyRef.current = false; } keysRef.current.push(event.key); } if (event.key === "{") { const braceTime = performance.now(); console.log(`⏱️ [KEYBOARD] Left brace "{" detected at: ${new Date().toISOString()}`); console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(braceTime - listenerRegisterStartTime).toFixed(2)}ms`); leftBraceCountRef.current += 1; } else if (event.key === "}") { const braceTime = performance.now(); console.log(`⏱️ [KEYBOARD] Right brace "}" detected at: ${new Date().toISOString()}`); console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(braceTime - listenerRegisterStartTime).toFixed(2)}ms`); rightBraceCountRef.current += 1; // ✅ OPTIMIZED: Check for complete QR immediately and update state only once if (leftBraceCountRef.current === rightBraceCountRef.current && leftBraceCountRef.current > 0) { const completeTime = performance.now(); console.log(`⏱️ [KEYBOARD] Complete QR detected immediately! Time: ${completeTime.toFixed(2)}ms`); console.log(`⏰ [KEYBOARD] Complete QR at: ${new Date().toISOString()}`); const qrValue = keysRef.current.join("").substring( keysRef.current.indexOf("{"), keysRef.current.lastIndexOf("}") + 1 ); console.log(`⏱️ [KEYBOARD] QR value: ${qrValue}`); // ✅ TABLET OPTIMIZATION: Directly set qrCodeScannerValues without any state chain // Use flushSync for immediate update on tablets (if available, otherwise use regular setState) setQrCodeScannerValues((value) => { console.log(`⏱️ [KEYBOARD] Setting qrCodeScannerValues directly: ${qrValue}`); return [...value, qrValue]; }); // Reset scanner input immediately (using refs, no state update) keysRef.current = []; leftBraceCountRef.current = 0; rightBraceCountRef.current = 0; isFirstKeyRef.current = true; // ✅ TABLET OPTIMIZATION: Defer all cleanup state updates to avoid blocking // Use setTimeout to ensure QR processing happens first setTimeout(() => { startTransition(() => { setKeys([]); setLeftCurlyBraceCount(0); setRightCurlyBraceCount(0); setScanState("pending"); resetScannerInput(); }); }, 0); return; } } // ✅ TABLET OPTIMIZATION: Completely skip state updates during scanning // Only update state for the first brace detection (for UI feedback) // All other updates are deferred to avoid blocking on tablets if (leftBraceCountRef.current === 1 && keysRef.current.length === 1 && event.key === "{") { // Only update state once when first brace is detected startTransition(() => { setKeys([...keysRef.current]); setLeftCurlyBraceCount(leftBraceCountRef.current); setRightCurlyBraceCount(rightBraceCountRef.current); }); } // Skip all other state updates during scanning to maximize performance on tablets }; document.addEventListener("keydown", handleKeyDown); const listenerRegisterTime = performance.now() - listenerRegisterStartTime; console.log(`⏱️ [KEYBOARD LISTENER] Listener registered in: ${listenerRegisterTime.toFixed(2)}ms`); console.log(`⏰ [KEYBOARD LISTENER] Listener ready at: ${new Date().toISOString()}`); return () => { console.log(`⏱️ [KEYBOARD LISTENER] Removing keyboard listener at: ${new Date().toISOString()}`); document.removeEventListener("keydown", handleKeyDown); }; } else { console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Scanner not active, skipping listener registration`); } const effectTime = performance.now() - effectStartTime; console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Total effect time: ${effectTime.toFixed(2)}ms`); }, [isScanning]); // ✅ OPTIMIZED: Simplify the QR scanner effect - it's now mainly for initial detection useEffect(() => { const effectStartTime = performance.now(); console.log(`⏱️ [QR SCANNER EFFECT] Triggered at: ${new Date().toISOString()}`); console.log(`⏱️ [QR SCANNER EFFECT] Keys count: ${keys.length}, leftBrace: ${leftCurlyBraceCount}, rightBrace: ${rightCurlyBraceCount}`); if (rightCurlyBraceCount > leftCurlyBraceCount || leftCurlyBraceCount > 1) { // Prevent multiple scan setScanState("retry"); setScanError("Too many scans at once"); resetQrCodeScanner("Too many scans at once"); } else { // Only show "scanning" state when first brace is detected if (leftCurlyBraceCount == 1 && keys.length == 1) { const scanDetectedTime = performance.now(); setScanState("scanning"); console.log(`%c Scan detected, waiting for inputs...`, "color:cyan"); console.log(`⏱️ [QR SCANNER] Scan detected time: ${scanDetectedTime.toFixed(2)}ms`); console.log(`⏰ [QR SCANNER] Scan detected at: ${new Date().toISOString()}`); } // Note: Complete QR detection is now handled directly in handleKeyDown // This effect is mainly for UI feedback and error handling } }, [keys, leftCurlyBraceCount, rightCurlyBraceCount]); useEffect(() => { if (qrCodeScannerValues.length > 0) { const processStartTime = performance.now(); console.log(`⏱️ [QR SCANNER PROCESS] Processing qrCodeScannerValues at: ${new Date().toISOString()}`); console.log(`⏱️ [QR SCANNER PROCESS] Values count: ${qrCodeScannerValues.length}`); const scannedValues = qrCodeScannerValues[0]; console.log(`%c Scanned Result: `, "color:cyan", scannedValues); console.log(`⏱️ [QR SCANNER PROCESS] Scanned value: ${scannedValues}`); console.log(`⏰ [QR SCANNER PROCESS] Processing at: ${new Date().toISOString()}`); if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING // 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式 // 这些格式需要传递完整值给 processQrCode 处理 if (scannedValues.length > 9) { const ninthChar = scannedValues.substring(8, 9); if (ninthChar === "e" || ninthChar === "u") { // {2fiteste数字} 或 {2fitestu任何内容} 格式 console.log(`%c DEBUG: detected shortcut format: `, "color:pink", scannedValues); const debugValue = { value: scannedValues // 传递完整值,让 processQrCode 处理 } setScanResult(debugValue); const processTime = performance.now() - processStartTime; console.log(`⏱️ [QR SCANNER PROCESS] Shortcut processing time: ${processTime.toFixed(2)}ms`); return; } } // 原有的 {2fitest数字} 格式(纯数字,向后兼容) const number = scannedValues.substring(8, scannedValues.length - 1); if (/^\d+$/.test(number)) { // Check if number contains only digits console.log(`%c DEBUG: detected ID: `, "color:pink", number); const debugValue = { value: number } setScanResult(debugValue); const processTime = performance.now() - processStartTime; console.log(`⏱️ [QR SCANNER PROCESS] ID processing time: ${processTime.toFixed(2)}ms`); return; } else { // 如果不是纯数字,传递完整值让 processQrCode 处理 const debugValue = { value: scannedValues } setScanResult(debugValue); const processTime = performance.now() - processStartTime; console.log(`⏱️ [QR SCANNER PROCESS] Non-numeric processing time: ${processTime.toFixed(2)}ms`); return; } } try { const parseStartTime = performance.now(); const data: QrCodeInfo = JSON.parse(scannedValues); const parseTime = performance.now() - parseStartTime; console.log(`%c Parsed scan data`, "color:green", data); console.log(`⏱️ [QR SCANNER PROCESS] JSON parse time: ${parseTime.toFixed(2)}ms`); const content = scannedValues.substring(1, scannedValues.length - 1); data.value = content; const setResultStartTime = performance.now(); setScanResult(data); const setResultTime = performance.now() - setResultStartTime; console.log(`⏱️ [QR SCANNER PROCESS] setScanResult time: ${setResultTime.toFixed(2)}ms`); console.log(`⏰ [QR SCANNER PROCESS] setScanResult at: ${new Date().toISOString()}`); const processTime = performance.now() - processStartTime; console.log(`⏱️ [QR SCANNER PROCESS] Total processing time: ${processTime.toFixed(2)}ms`); } catch (error) { // Rough match for other scanner input -- Pending Review console.log(`⏱️ [QR SCANNER PROCESS] JSON parse failed, trying rough match`); const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; if (silId == 0) { const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0; setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()}); } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); } resetQrCodeScanner(String(error)); } // resetQrCodeScanner(); } }, [qrCodeScannerValues]); return ( {children} ); }; export const useQrCodeScannerContext = (): QrCodeScanner => { const context = useContext(QrCodeScannerContext); if (!context) { throw new Error( "useQrCodeScanner must be used within a QrCodeScannerProvider", ); } return context; }; export default QrCodeScannerProvider;