| @@ -443,6 +443,7 @@ export interface UpdatePickExecutionIssueRequest { | |||
| export interface StoreLaneSummary { | |||
| storeId: string; | |||
| rows: LaneRow[]; | |||
| defaultTruckCount: number | null; | |||
| } | |||
| export interface LaneRow { | |||
| @@ -606,16 +607,22 @@ export const updatePickExecutionIssueStatus = async ( | |||
| }; | |||
| export async function fetchStoreLaneSummary(storeId: string, requiredDate?: string, releaseType?: string): Promise<StoreLaneSummary> { | |||
| const dateToUse = requiredDate || dayjs().format('YYYY-MM-DD'); | |||
| const url = `${BASE_API_URL}/doPickOrder/summary-by-store?storeId=${encodeURIComponent(storeId)}&requiredDate=${encodeURIComponent(dateToUse)}&releaseType=${encodeURIComponent(releaseType || 'all')}`; | |||
| const response = await serverFetchJson<StoreLaneSummary>( | |||
| url, | |||
| { | |||
| const label = `[API] fetchStoreLaneSummary ${storeId}`; | |||
| console.time(label); | |||
| try { | |||
| const response = await serverFetchJson<StoreLaneSummary>(url, { | |||
| method: "GET", | |||
| cache: "no-store", | |||
| next: { revalidate: 0 } | |||
| } | |||
| ); | |||
| return response; | |||
| next: { revalidate: 0 }, | |||
| }); | |||
| console.timeEnd(label); | |||
| return response; | |||
| } catch (error) { | |||
| console.error(`[API] Error in fetchStoreLaneSummary ${storeId}:`, error); | |||
| throw error; | |||
| } | |||
| } | |||
| // 按车道分配订单 | |||
| @@ -1414,6 +1421,22 @@ export const fetchReleasedDoPickOrdersForSelection = async ( | |||
| }); | |||
| return response ?? []; | |||
| }; | |||
| export const fetchReleasedDoPickOrdersForSelectionToday = async ( | |||
| shopName?: string, | |||
| storeId?: string, | |||
| truck?: string | |||
| ): Promise<ReleasedDoPickOrderListItem[]> => { | |||
| const params = new URLSearchParams(); | |||
| if (shopName?.trim()) params.append("shopName", shopName.trim()); | |||
| if (storeId?.trim()) params.append("storeId", storeId.trim()); | |||
| if (truck?.trim()) params.append("truck", truck.trim()); | |||
| const query = params.toString(); | |||
| const url = `${BASE_API_URL}/doPickOrder/released-today${query ? `?${query}` : ""}`; | |||
| const response = await serverFetchJson<ReleasedDoPickOrderListItem[]>(url, { | |||
| method: "GET", | |||
| }); | |||
| return response ?? []; | |||
| }; | |||
| export const fetchReleasedDoPickOrderCountByStore = async ( | |||
| storeId: string | |||
| ): Promise<number> => { | |||
| @@ -1,7 +1,7 @@ | |||
| "use client"; | |||
| import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material"; | |||
| import { useCallback, useEffect, useState } from "react"; | |||
| import { useCallback, useEffect, useState, useRef } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| @@ -21,18 +21,48 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const [selectedStore, setSelectedStore] = useState<string>("2/F"); | |||
| const [selectedTruck, setSelectedTruck] = useState<string>(""); | |||
| const [selectedDefaultTruck, setSelectedDefaultTruck] = useState<string>(""); | |||
| const [modalOpen, setModalOpen] = useState(false); | |||
| const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]); | |||
| const [truckCounts4F, setTruckCounts4F] = useState<{ truck: string; count: number }[]>([]); | |||
| const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null); | |||
| const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null); | |||
| // 其他 state 旁邊加一組: | |||
| const hasLoggedRef = useRef(false); | |||
| const fullReadyLoggedRef = useRef(false); | |||
| const pendingRef = useRef(0); | |||
| const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("today"); | |||
| const [isLoadingSummary, setIsLoadingSummary] = useState(false); | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| const [isDefaultTruck, setIsDefaultTruck] = useState(false); | |||
| //const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const defaultTruckCount = summary4F?.defaultTruckCount ?? 0; | |||
| const [beforeTodayTruckXCount, setBeforeTodayTruckXCount] = useState(0); | |||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const [releaseType, setReleaseType] = useState<string>("batch"); | |||
| const startFullTimer = () => { | |||
| if (typeof window === "undefined") return; | |||
| const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const; | |||
| if (!(window as any)[key]) { | |||
| (window as any)[key] = true; | |||
| console.time("[FG] FloorLanePanel full ready"); | |||
| } | |||
| }; | |||
| const tryEndFullTimer = () => { | |||
| if (typeof window === "undefined") return; | |||
| const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const; | |||
| if ((window as any)[key] && !fullReadyLoggedRef.current && pendingRef.current === 0) { | |||
| fullReadyLoggedRef.current = true; | |||
| console.timeEnd("[FG] FloorLanePanel full ready"); | |||
| delete (window as any)[key]; | |||
| } | |||
| }; | |||
| const loadSummaries = useCallback(async () => { | |||
| setIsLoadingSummary(true); | |||
| pendingRef.current += 1; | |||
| startFullTimer(); | |||
| try { | |||
| // Convert selectedDate to the format needed | |||
| let dateParam: string | undefined; | |||
| @@ -54,15 +84,33 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| console.error("Error loading summaries:", error); | |||
| } finally { | |||
| setIsLoadingSummary(false); | |||
| // ⭐ 新增:this async 结束,pending--,尝试结束 full ready 计时 | |||
| pendingRef.current -= 1; | |||
| tryEndFullTimer(); | |||
| if (!hasLoggedRef.current) { | |||
| hasLoggedRef.current = true; | |||
| if (typeof window !== "undefined") { | |||
| const key = "__FG_FLOOR_PANEL_TIMER_STARTED__" as const; | |||
| if ((window as any)[key]) { | |||
| console.timeEnd("[FG] FloorLanePanel initial load"); | |||
| delete (window as any)[key]; | |||
| } else { | |||
| console.log("Timer '[FG] FloorLanePanel initial load' already ended or never started, skip."); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }, [selectedDate, releaseType]); | |||
| // 初始化 | |||
| useEffect(() => { | |||
| loadSummaries(); | |||
| }, [loadSummaries]); | |||
| useEffect(() => { | |||
| const loadCounts = async () => { | |||
| pendingRef.current += 1; | |||
| startFullTimer(); | |||
| try { | |||
| const [list2F, list4F] = await Promise.all([ | |||
| fetchReleasedDoPickOrdersForSelection(undefined, "2/F"), | |||
| @@ -84,10 +132,36 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| console.error("Error loading counts:", e); | |||
| setTruckCounts2F([]); | |||
| setTruckCounts4F([]); | |||
| }finally { | |||
| // ⭐ 新增:结束时 pending--,尝试结束 full ready 计时 | |||
| pendingRef.current -= 1; | |||
| tryEndFullTimer(); | |||
| } | |||
| }; | |||
| loadCounts(); | |||
| }, [loadSummaries]); | |||
| useEffect(() => { | |||
| const loadBeforeTodayTruckX = async () => { | |||
| pendingRef.current += 1; | |||
| startFullTimer(); | |||
| try { | |||
| const list = await fetchReleasedDoPickOrdersForSelection( | |||
| undefined, // shopName | |||
| undefined, // storeId: Truck X 的 store_id 是 null | |||
| "車線-X" // 只看 Truck X | |||
| ); | |||
| setBeforeTodayTruckXCount(list.length); | |||
| } catch (e) { | |||
| console.error("Error loading beforeTodayTruckX:", e); | |||
| setBeforeTodayTruckXCount(0); | |||
| }finally { | |||
| // ⭐ 新增:结束时 pending--,尝试结束 full ready 计时 | |||
| pendingRef.current -= 1; | |||
| tryEndFullTimer(); | |||
| } | |||
| }; | |||
| loadBeforeTodayTruckX(); | |||
| }, []); | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| @@ -299,7 +373,7 @@ const getDateLabel = (offset: number) => { | |||
| {/* Grid containing both floors */} | |||
| <Grid container spacing={2}> | |||
| <Grid container spacing={2}> | |||
| {/* 2/F 楼层面板 */} | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| @@ -499,6 +573,45 @@ const getDateLabel = (offset: number) => { | |||
| </Box> | |||
| </Stack> | |||
| </Grid> | |||
| {/* 4/F Today default lane*/} | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")}</Typography> | |||
| <Box | |||
| sx={{ | |||
| border: '1px solid #e0e0e0', | |||
| borderRadius: 1, | |||
| p: 1, | |||
| backgroundColor: '#fafafa', | |||
| flex: 1 | |||
| }} | |||
| > | |||
| {defaultTruckCount === 0 ? ( | |||
| <Typography >{t("No entries available")}</Typography> | |||
| ) : ( | |||
| <Button | |||
| variant="outlined" | |||
| size="medium" | |||
| onClick={() => { | |||
| // Truck X 綁 4/F,如果你要放在 4/F 區塊 | |||
| setSelectedStore(""); | |||
| // 真正的 Truck lane code:車線-X | |||
| setSelectedTruck("車線-X"); | |||
| // 告訴 modal 這是 default truck 模式 | |||
| setIsDefaultTruck(true); | |||
| // 打開 modal | |||
| setModalOpen(true); | |||
| setDefaultDateScope("today"); | |||
| }} | |||
| > | |||
| {`${t("Truck X")} (${defaultTruckCount})`} | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| </Stack> | |||
| </Grid> | |||
| {/* 2/F 未完成已放單 - 與上方相同 UI */} | |||
| <Grid item xs={12}> | |||
| <Box | |||
| @@ -652,10 +765,43 @@ const getDateLabel = (offset: number) => { | |||
| </Box> | |||
| </Stack> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2}> | |||
| <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")} </Typography> | |||
| <Box | |||
| sx={{ | |||
| border: '1px solid #e0e0e0', | |||
| borderRadius: 1, | |||
| p: 1, | |||
| backgroundColor: '#fafafa', | |||
| flex: 1 | |||
| }} | |||
| > | |||
| {beforeTodayTruckXCount === 0 ? ( | |||
| <Typography>{t("No entries available")}</Typography> | |||
| ) : ( | |||
| <Button | |||
| variant="outlined" | |||
| size="medium" | |||
| onClick={() => { | |||
| setSelectedStore("4/F"); // 或用專門標示 Truck X | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("before"); // 類似上一輪說的 dateScope | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| {`${t("Truck X")} (${beforeTodayTruckXCount})`} | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| </Stack> | |||
| </Grid> | |||
| <ReleasedDoPickOrderSelectModal | |||
| open={modalOpen} | |||
| storeId={selectedStore} | |||
| truck={selectedTruck} | |||
| isDefaultTruck={isDefaultTruck} | |||
| onClose={() => setModalOpen(false)} | |||
| onAssigned={() => { | |||
| loadSummaries(); | |||
| @@ -82,8 +82,27 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo | |||
| ); | |||
| const [fgPickOrdersData, setFgPickOrdersData] = useState<FGPickOrderResponse[]>([]); | |||
| useEffect(() => { | |||
| if (typeof window === "undefined") return; | |||
| const key = "__FG_PAGE_READY_TIMER_STARTED__" as const; | |||
| if ((window as any)[key]) { | |||
| console.log("Timer '[FinishedGoodSearch] page ready' already started, skip."); | |||
| return; | |||
| } | |||
| (window as any)[key] = true; | |||
| console.time("[FinishedGoodSearch] page ready"); | |||
| }, []); | |||
| const [releasedOrderCount, setReleasedOrderCount] = useState<number>(0); | |||
| useEffect(() => { | |||
| console.time("[FinishedGoodSearch] initial render"); | |||
| return () => { | |||
| console.timeEnd("[FinishedGoodSearch] initial render"); | |||
| }; | |||
| }, []); | |||
| const fetchReleasedOrderCount = useCallback(async () => { | |||
| try { | |||
| const releasedOrders = await fetchReleasedDoPickOrders(); | |||
| @@ -702,6 +721,18 @@ const handleAssignByLane = useCallback(async ( | |||
| filterArgs={filterArgs} | |||
| onFgPickOrdersChange={setFgPickOrdersData} | |||
| onSwitchToDetailTab={handleSwitchToDetailTab} | |||
| onFirstLoadDone={() => { | |||
| if (typeof window === "undefined") return; | |||
| const key = "__FG_PAGE_READY_TIMER_STARTED__" as const; | |||
| // 只在计时器真的存在时才调用 timeEnd,避免 "does not exist" | |||
| if ((window as any)[key]) { | |||
| console.timeEnd("[FinishedGoodSearch] page ready"); | |||
| delete (window as any)[key]; | |||
| } else { | |||
| console.log("Timer '[FinishedGoodSearch] page ready' was already ended, skip."); | |||
| } | |||
| }} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && ( | |||
| @@ -59,6 +59,7 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void; | |||
| onSwitchToDetailTab?: () => void; | |||
| onFirstLoadDone?: () => void; | |||
| } | |||
| // QR Code Modal Component (from LotTable) | |||
| @@ -72,7 +73,7 @@ const QrCodeModal: React.FC<{ | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const [manualInput, setManualInput] = useState<string>(''); | |||
| const floorPanelTimerStartedRef = useRef(false); | |||
| const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); | |||
| const [manualInputError, setManualInputError] = useState<boolean>(false); | |||
| const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false); | |||
| @@ -315,7 +316,7 @@ const QrCodeModal: React.FC<{ | |||
| ); | |||
| }; | |||
| const PickExecution: React.FC<Props> = ({ filterArgs, onFgPickOrdersChange, onSwitchToDetailTab }) => { | |||
| const PickExecution: React.FC<Props> = ({ filterArgs, onFgPickOrdersChange, onSwitchToDetailTab, onFirstLoadDone }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const router = useRouter(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -353,7 +354,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| // Add QR modal states | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | |||
| const floorPanelTimerStartedRef = useRef(false); | |||
| // Add GoodPickExecutionForm states | |||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | |||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | |||
| @@ -523,12 +524,18 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| if (session && currentUserId && !initializationRef.current) { | |||
| console.log(" Session loaded, initializing pick order..."); | |||
| initializationRef.current = true; | |||
| // Only fetch existing data, no auto-assignment | |||
| fetchAllCombinedLotData(); | |||
| (async () => { | |||
| try { | |||
| await fetchAllCombinedLotData(); // ✅ 等待数据加载完成 | |||
| } finally { | |||
| if (onFirstLoadDone) { | |||
| onFirstLoadDone(); // ✅ 这时候再结束 [FinishedGoodSearch] page ready | |||
| } | |||
| } | |||
| })(); | |||
| } | |||
| }, [session, currentUserId, fetchAllCombinedLotData]); | |||
| }, [session, currentUserId, fetchAllCombinedLotData, onFirstLoadDone]); | |||
| // Add event listener for manual assignment | |||
| useEffect(() => { | |||
| const handlePickOrderAssigned = () => { | |||
| @@ -823,7 +830,24 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [qrValues, combinedLotData, handleQrCodeSubmit]); | |||
| useEffect(() => { | |||
| if (typeof window === "undefined") return; | |||
| const key = "__FG_FLOOR_PANEL_TIMER_STARTED__" as const; | |||
| // 只有当「没有 FG 订单」时才会显示 FinishedGoodFloorLanePanel | |||
| if (fgPickOrders.length === 0 && !fgPickOrdersLoading) { | |||
| if (!(window as any)[key]) { | |||
| (window as any)[key] = true; | |||
| console.time("[FG] FloorLanePanel initial load"); | |||
| } | |||
| } | |||
| // 如果之后拿到 FG 订单,你可以选择在这里清掉标记(可选) | |||
| if (fgPickOrders.length > 0 && (window as any)[key]) { | |||
| // delete (window as any)[key]; | |||
| } | |||
| }, [fgPickOrders.length, fgPickOrdersLoading]); | |||
| const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { | |||
| if (value === '' || value === null || value === undefined) { | |||
| setPickQtyData(prev => ({ | |||
| @@ -22,18 +22,21 @@ import { | |||
| fetchReleasedDoPickOrdersForSelection, | |||
| assignByDoPickOrderId, | |||
| type ReleasedDoPickOrderListItem, | |||
| fetchReleasedDoPickOrdersForSelectionToday, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import Swal from "sweetalert2"; | |||
| import dayjs from "dayjs"; | |||
| type DateScope = "today" | "before"; | |||
| interface Props { | |||
| open: boolean; | |||
| onClose: () => void; | |||
| onAssigned: () => void; | |||
| storeId: string; | |||
| truck: string; | |||
| isDefaultTruck: boolean; | |||
| defaultDateScope?: "today" | "before"; | |||
| } | |||
| const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| @@ -42,11 +45,12 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| onAssigned, | |||
| storeId, | |||
| truck, | |||
| isDefaultTruck, | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("today"); | |||
| const [list, setList] = useState<ReleasedDoPickOrderListItem[]>([]); | |||
| const [loading, setLoading] = useState(false); | |||
| const [shopSearch, setShopSearch] = useState(""); | |||
| @@ -56,11 +60,33 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| if (!open) return; | |||
| setLoading(true); | |||
| try { | |||
| const data = await fetchReleasedDoPickOrdersForSelection( | |||
| shopSearch.trim() || undefined, | |||
| storeId, | |||
| truck?.trim() || undefined // 傳入 truck | |||
| ); | |||
| let data: ReleasedDoPickOrderListItem[]; // ⭐ 先宣告 | |||
| if (isDefaultTruck) { | |||
| if (defaultDateScope === "today") { | |||
| // Truck X 今天 | |||
| data = await fetchReleasedDoPickOrdersForSelectionToday( | |||
| undefined, // shopName | |||
| undefined, // storeId | |||
| "車線-X" // truck | |||
| ); | |||
| } else { | |||
| // Truck X 以前(/released,server 內是 < today) | |||
| data = await fetchReleasedDoPickOrdersForSelection( | |||
| undefined, | |||
| undefined, | |||
| "車線-X" | |||
| ); | |||
| } | |||
| } else { | |||
| // 一般車道 | |||
| data = await fetchReleasedDoPickOrdersForSelection( | |||
| shopSearch.trim() || undefined, | |||
| storeId, | |||
| truck?.trim() || undefined | |||
| ); | |||
| } | |||
| setList(data); | |||
| } catch (e) { | |||
| console.error(e); | |||
| @@ -68,7 +94,7 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [open, shopSearch, storeId, truck]); | |||
| }, [open, shopSearch, storeId, truck, isDefaultTruck, defaultDateScope]); | |||
| useEffect(() => { | |||
| loadList(); | |||
| @@ -213,7 +213,7 @@ const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | nul | |||
| sx={{ | |||
| border: '3px solid #135fed', | |||
| overflowX: 'auto', | |||
| maxHeight: 440, | |||
| maxHeight: 540, | |||
| overflow: 'auto' | |||
| }} | |||
| > | |||
| @@ -57,7 +57,7 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| setScanResult(undefined); | |||
| resetScannerInput(); | |||
| console.log("%c Scanner Reset", "color:cyan"); | |||
| //console.log("%c Scanner Reset", "color:cyan"); | |||
| if (error.length > 0) { | |||
| console.log("%c Error:", "color:red", error); | |||
| @@ -68,25 +68,25 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| const startQrCodeScanner = useCallback(() => { | |||
| const startTime = performance.now(); | |||
| console.log(`⏱️ [SCANNER START] Called at: ${new Date().toISOString()}`); | |||
| //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`); | |||
| //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`); | |||
| //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()}`); | |||
| //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"); | |||
| //console.log("%c Scanning stopped ", "color:cyan"); | |||
| }, []); | |||
| // Find by rough match, return 0 if not found | |||
| @@ -127,12 +127,12 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| useEffect(() => { | |||
| const effectStartTime = performance.now(); | |||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Triggered at: ${new Date().toISOString()}`); | |||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] isScanning: ${isScanning}`); | |||
| //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()}`); | |||
| //console.log(`⏱️ [KEYBOARD LISTENER] Registering keyboard listener at: ${new Date().toISOString()}`); | |||
| // Reset refs when starting scan | |||
| keysRef.current = []; | |||
| @@ -147,9 +147,9 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| // ✅ 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`); | |||
| //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); | |||
| @@ -157,20 +157,20 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| 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`); | |||
| //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`); | |||
| //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()}`); | |||
| //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("{"), | |||
| @@ -223,26 +223,26 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| 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()}`); | |||
| //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()}`); | |||
| // 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`); | |||
| //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`); | |||
| //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}`); | |||
| //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"); | |||
| @@ -254,9 +254,9 @@ useEffect(() => { | |||
| { | |||
| 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()}`); | |||
| // 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 | |||
| @@ -266,13 +266,13 @@ useEffect(() => { | |||
| 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}`); | |||
| // 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()}`); | |||
| //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...} 格式 | |||
| @@ -310,7 +310,7 @@ useEffect(() => { | |||
| } | |||
| setScanResult(debugValue); | |||
| const processTime = performance.now() - processStartTime; | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Non-numeric processing time: ${processTime.toFixed(2)}ms`); | |||
| // console.log(`⏱️ [QR SCANNER PROCESS] Non-numeric processing time: ${processTime.toFixed(2)}ms`); | |||
| return; | |||
| } | |||
| } | |||
| @@ -319,8 +319,8 @@ useEffect(() => { | |||
| 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`); | |||
| // 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; | |||
| @@ -328,14 +328,14 @@ useEffect(() => { | |||
| 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()}`); | |||
| // 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`); | |||
| // 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`); | |||
| //console.log(`⏱️ [QR SCANNER PROCESS] JSON parse failed, trying rough match`); | |||
| const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; | |||
| if (silId == 0) { | |||
| @@ -39,6 +39,8 @@ | |||
| "Lines Per Pick Order": "每提料單行數", | |||
| "Pick Orders Details": "提料單詳情", | |||
| "Lines": "行數", | |||
| "Before Today": "以前", | |||
| "Truck X": "車線-X", | |||
| "Finsihed good items": "成品項目", | |||
| "kinds": "款", | |||
| "Completed Date": "完成日期", | |||