From 0132bbd30f414a952cf0fdc960177836a84de956 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 15 Jun 2026 17:22:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=90=E5=93=81=E5=87=BA=E5=80=89=E5=9F=B7?= =?UTF-8?q?=E8=B2=A8=E6=99=82=20=E6=A8=99=E7=B1=A4=E5=88=97=E5=8D=B0?= =?UTF-8?q?=E6=99=82=E9=A0=81=E6=95=B8=E9=A1=AF=E7=A4=BA=E7=A9=BA=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/do/actions.tsx | 89 +- .../DoWorkbench/DoWorkbenchPickShell.tsx | 4 + .../DoWorkbench/DoWorkbenchTabs.tsx | 32 +- .../WorkbenchGoodPickExecutionDetail.tsx | 969 ++++++--------- .../UserSearch/UserExcelSheetView.tsx | 240 +++- src/i18n/zh/pickOrder.json | 1093 ++++++++--------- 6 files changed, 1120 insertions(+), 1307 deletions(-) diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index 549f218..e336cd1 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -27,10 +27,6 @@ export interface DoDetail { status: string; /** 加單 DO */ isExtra?: boolean; - /** 揀貨員名稱(delivery_order_pick_order.handlerName) */ - handlerName?: string | null; - /** 來源 DO 車線 */ - truckLaneCode?: string | null; deliveryOrderLines: DoDetailLine[]; } @@ -38,8 +34,6 @@ export interface DoDetailLine { id: number; itemNo: string; qty: number; - /** Sum of stock_out_line qty for linked pick order line; falls back to qty. */ - actualShippedQty?: number; price: number; status: string; itemName?: string; @@ -62,7 +56,6 @@ export interface DoSearchAll { shopName: string; shopAddress?: string; isExtra?: boolean; - truckLanceCode?: string | null; } export interface DoSearchLiteResponse { records: DoSearchAll[]; @@ -535,6 +528,7 @@ export interface PrintWorkbenchDNLabelsRequest{ printerId: number; printQty: number; numOfCarton: number; + blankCartonNumber?: boolean; } export interface PrintWorkbenchDNLabelsReprintRequest{ deliveryOrderPickOrderId: number; @@ -584,6 +578,9 @@ export async function printDNLabelsWorkbench(request: PrintWorkbenchDNLabelsRequ params.append("printQty", request.printQty.toString()); } params.append("numOfCarton", request.numOfCarton.toString()); + if (request.blankCartonNumber) { + params.append("blankCartonNumber", "true"); + } await serverFetchWithNoContent(`${BASE_API_URL}/doPickOrder/workbench/print-DNLabels?${params.toString()}`,{ method: "GET" @@ -675,81 +672,3 @@ export async function fetchAllDoSearch( return data.records; } - -export interface SubmitDoReplenishmentLineRequest { - deliveryDate: string; - sourceDoId: number; - sourceDoLineId: number; - replenishQty: number; - truckLaneCode?: string; - reason?: string; -} - -export interface DoReplenishmentRecord { - id: number; - code: string; - deliveryDate: string; - sourceDoId: number; - sourceDoCode?: string; - sourceDoLineId: number; - itemId: number; - itemNo?: string; - itemName?: string; - originalQty?: number; - replenishQty: number; - shortUom?: string; - shopCode?: string; - shopName?: string; - truckLaneCode?: string; - targetDoId?: number; - targetDoCode?: string; - targetDoEstimatedArrivalDate?: string; - pickOrderLineId?: number; - status: string; - reason?: string; - created?: string; -} - -export async function submitDoReplenishment( - lines: SubmitDoReplenishmentLineRequest[], -): Promise { - return serverFetchJson(`${BASE_API_URL}/do/replenishment`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ lines }), - }); -} - -export async function fetchDoReplenishmentList(params: { - deliveryDate?: string; - status?: string; -}): Promise { - const query = convertObjToURLSearchParams({ - deliveryDate: params.deliveryDate || undefined, - status: params.status && params.status !== "all" ? params.status : undefined, - }); - const url = query - ? `${BASE_API_URL}/do/replenishment?${query}` - : `${BASE_API_URL}/do/replenishment`; - return serverFetchJson(url, { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); -} - -export async function fetchDoReplenishmentForBatchRelease(params: { - truckLaneCode?: string; - shopName?: string; -}): Promise { - const query = convertObjToURLSearchParams({ - truckLaneCode: params.truckLaneCode?.trim() || undefined, - shopName: params.shopName?.trim() || undefined, - }); - return serverFetchJson( - `${BASE_API_URL}/do/replenishment/for-batch-release?${query}`, - { - method: "GET", - headers: { "Content-Type": "application/json" }, - }, - ); -} diff --git a/src/components/DoWorkbench/DoWorkbenchPickShell.tsx b/src/components/DoWorkbench/DoWorkbenchPickShell.tsx index 078a782..5f1e2d2 100644 --- a/src/components/DoWorkbench/DoWorkbenchPickShell.tsx +++ b/src/components/DoWorkbench/DoWorkbenchPickShell.tsx @@ -11,6 +11,7 @@ import { import WorkbenchFloorLanePanel from "./WorkbenchFloorLanePanel"; import WorkbenchGoodPickExecutionDetail from "./WorkbenchGoodPickExecutionDetail"; import type { WorkbenchLanePanelPrefs } from "./workbenchLanePanelPrefs"; +import type { PrinterCombo } from "@/app/api/settings/printer"; export type DoWorkbenchPickShellLaneMode = "normal" | "etra"; @@ -19,6 +20,7 @@ type DoWorkbenchPickShellProps = { laneMode?: DoWorkbenchPickShellLaneMode; lanePanelPrefs: WorkbenchLanePanelPrefs; onLanePanelPrefsChange: (prefs: WorkbenchLanePanelPrefs) => void; + labelPrinter?: PrinterCombo | null; }; /** @@ -28,6 +30,7 @@ const DoWorkbenchPickShell: React.FC = ({ laneMode = "normal", lanePanelPrefs, onLanePanelPrefsChange, + labelPrinter = null, }) => { const { data: session, status } = useSession() as { data: SessionWithTokens | null; @@ -118,6 +121,7 @@ const DoWorkbenchPickShell: React.FC = ({ )} diff --git a/src/components/DoWorkbench/DoWorkbenchTabs.tsx b/src/components/DoWorkbench/DoWorkbenchTabs.tsx index 67eb27a..3e863ac 100644 --- a/src/components/DoWorkbench/DoWorkbenchTabs.tsx +++ b/src/components/DoWorkbench/DoWorkbenchTabs.tsx @@ -33,12 +33,10 @@ import { DEFAULT_WORKBENCH_LANE_PANEL_PREFS, type WorkbenchLanePanelPrefs, } from "./workbenchLanePanelPrefs"; -import { - normalizeWorkbenchTabFromUrl, - WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE, -} from "./workbenchTabConstants"; -/** Backend Etra summary: each lane `total` = incomplete (`pending`/`released`) tickets for that day. */ +const ALLOWED_WORKBENCH_TABS = new Set([0, 1, 2, 3, 4, 5, 6]); + +/** Backend Etra summary: each lane `total` = distinct incomplete (`pending`/`released`) `delivery_order_pick_order` rows for that day. */ function sumIncompleteEtraDopoTickets(groups: WorkbenchEtraShopLaneGroup[]): number { let n = 0; for (const g of groups) { @@ -85,8 +83,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom const [labelPrinter, setLabelPrinter] = React.useState(null); const [releasedOrderCount, setReleasedOrderCount] = React.useState(0); const [etraIncompleteDopoCount, setEtraIncompleteDopoCount] = React.useState(0); - const { t } = useTranslation(); - + const { t } = useTranslation( ); const a4Printers = React.useMemo( () => (printerCombo || []).filter((printer) => printer.type === "A4"), [printerCombo], @@ -127,6 +124,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom return () => window.removeEventListener("pickOrderAssigned", onAssigned); }, [refreshWorkbenchCounts]); + /** Opening Etra tab refreshes badge (completion does not always dispatch `pickOrderAssigned`). */ const etraTabMountSkipRef = React.useRef(false); React.useEffect(() => { if (!etraTabMountSkipRef.current) { @@ -139,10 +137,8 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom React.useEffect(() => { if (urlTabStr == null || urlTabStr === "") return; const n = parseInt(urlTabStr, 10); - if (Number.isNaN(n)) return; - const normalized = normalizeWorkbenchTabFromUrl(n); - if (normalized != null) { - setTab(normalized); + if (!Number.isNaN(n) && ALLOWED_WORKBENCH_TABS.has(n)) { + setTab(n); } }, [urlTabStr]); @@ -151,7 +147,8 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom setTab(newTab); const params = new URLSearchParams(searchParams.toString()); params.set("tab", String(newTab)); - if (newTab !== WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE) { + /* ticketNo / targetDate deep-link only for "Finished Good Record" (mine) */ + if (newTab !== 2) { params.delete("ticketNo"); params.delete("targetDate"); } @@ -286,7 +283,10 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom /> )} /> - @@ -300,6 +300,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom columnGap: 2, rowGap: 1, }, + /* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */ "& .MuiTab-root": { overflow: "visible", minWidth: "auto", @@ -312,6 +313,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom value={1} sx={{ overflow: "visible", + /* 徽章在標籤右側外凸,預留空間避免與下一個 Tab 貼死 */ pr: etraIncompleteDopoCount > 99 ? 5 : etraIncompleteDopoCount > 0 ? 4 : 2, }} label={ @@ -365,6 +367,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom laneMode="normal" lanePanelPrefs={lanePanelPrefs} onLanePanelPrefsChange={setLanePanelPrefs} + labelPrinter={labelPrinter} /> @@ -372,6 +375,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom laneMode="etra" lanePanelPrefs={lanePanelPrefs} onLanePanelPrefsChange={setLanePanelPrefs} + labelPrinter={labelPrinter} /> @@ -402,6 +406,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom + ); }; @@ -419,3 +424,4 @@ const DoWorkbenchTabs: React.FC = (props) => ( ); export default DoWorkbenchTabs; + diff --git a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx index a6dbe2c..6c58f39 100644 --- a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx +++ b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx @@ -22,10 +22,8 @@ import { } from "@mui/material"; import dayjs from 'dayjs'; import { normalizeTargetDateInput } from "@/utils/workbenchTargetDate"; -import { isWorkbenchExtraTicket } from "@/utils/workbenchReleaseType"; import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodeProvider"; import { fetchLotDetail } from "@/app/api/inventory/actions"; -import { formatDepartureTime } from "@/app/utils/formatUtil"; import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; import { useTranslation } from "react-i18next"; import { usePathname, useRouter } from "next/navigation"; @@ -70,16 +68,13 @@ import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "@/components/FinishedGoodSearch/GoodPickExecutionForm"; -import { WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE } from "./workbenchTabConstants"; -import { - inferUnpickableScanAvailability, - translateWorkbenchRejectMessage, - type UnpickableScanAvailability, -} from "@/utils/workbenchPickLotUtils"; import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; import FGPickOrderCard from "@/components/FinishedGoodSearch/FGPickOrderCard"; import LinearProgressWithLabel from "@/components/common/LinearProgressWithLabel"; import ScanStatusAlert from "@/components/common/ScanStatusAlert"; +import Swal from "sweetalert2"; +import { printDNLabelsWorkbench } from "@/app/api/do/actions"; +import type { PrinterCombo } from "@/app/api/settings/printer"; interface Props { filterArgs: Record; @@ -87,143 +82,23 @@ interface Props { onRefreshReleasedOrderCount?: () => void; /** 階層揀貨資料已無(例如訂單已完成/釋放)時通知外層,以便重新檢查是否仍為「已指派」狀態 */ onWorkbenchHierarchyEmpty?: () => void; + labelPrinter?: PrinterCombo | null; } -type ProcessedStockOutLinesByItemId = Map>; - -function isLotRowPending(lot: any): boolean { - const st = String(lot?.stockOutLineStatus ?? "").toLowerCase(); - return ( - st === "pending" || - st === "partially_completed" || - st === "partially_complete" || - st === "" - ); -} - -function isStockOutLineAlreadyProcessed( - processedByItemId: ProcessedStockOutLinesByItemId, - itemId: number, - stockOutLineId: number | null | undefined, -): boolean { - const solId = Number(stockOutLineId); - if (!Number.isFinite(solId) || solId <= 0) return false; - return processedByItemId.get(itemId)?.has(solId) ?? false; -} - -function markProcessedStockOutLine( - prev: ProcessedStockOutLinesByItemId, - itemId: number, - stockOutLineId: number | null | undefined, -): ProcessedStockOutLinesByItemId { - const solId = Number(stockOutLineId); - if (!Number.isFinite(solId) || solId <= 0) return prev; - const newMap = new Map(prev); - if (!newMap.has(itemId)) newMap.set(itemId, new Set()); - newMap.get(itemId)!.add(solId); - return newMap; -} - -function parseWorkbenchQrPayload( - latestQr: string, -): { itemId: number; stockInLineId: number } | null { - try { - const qrData = JSON.parse(latestQr); - const itemId = Number(qrData?.itemId); - const stockInLineId = Number(qrData?.stockInLineId); - if (!Number.isFinite(itemId) || !Number.isFinite(stockInLineId)) return null; - return { itemId, stockInLineId }; - } catch { - return null; - } -} - -/** - * QR entry gate: pending / partial SOL not yet processed this session. - * Includes expired & unavailable rows (still need modal /换批); excludes completed/rejected/checked. - * `processedQrCombinations` still prevents re-picking the same SOL (e.g. two lines, same lot). - */ -function isLotRowEligibleForQrEntryGate( - lot: any, - itemId: number, - processedByItemId: ProcessedStockOutLinesByItemId, -): boolean { - if (Number(lot?.itemId) !== itemId) return false; - if (!isLotRowPending(lot)) return false; - const st = String(lot?.stockOutLineStatus ?? "").toLowerCase(); - if (st === "rejected" || st === "completed" || st === "checked") return false; - if (String(lot?.lotAvailability ?? "").toLowerCase() === "rejected") return false; - return !isStockOutLineAlreadyProcessed( - processedByItemId, - itemId, - lot.stockOutLineId, - ); -} - -function hasPendingUnprocessedRowForStockInLine( - indexes: { byStockInLineId: Map }, - itemId: number, - stockInLineId: number, - processedByItemId: ProcessedStockOutLinesByItemId, -): boolean { - const rows = indexes.byStockInLineId.get(stockInLineId) ?? []; - return rows.some((lot) => - isLotRowEligibleForQrEntryGate(lot, itemId, processedByItemId), - ); -} - -/** Any pending unprocessed SOL for this item (e.g. scan different stockInLineId → auto-switch). */ -function hasPendingUnprocessedRowForItem( - indexes: { byItemId: Map }, - itemId: number, - processedByItemId: ProcessedStockOutLinesByItemId, -): boolean { - const rows = indexes.byItemId.get(itemId) ?? []; - return rows.some((lot) => - isLotRowEligibleForQrEntryGate(lot, itemId, processedByItemId), - ); -} - -function findExactActiveMatchForStockInLine( - stockInLineLots: any[], - scannedItemId: number, - activeSuggestedLots: any[], - processedByItemId: ProcessedStockOutLinesByItemId, -): any | null { - const activeSet = new Set(activeSuggestedLots); - const candidates = stockInLineLots.filter( - (lot) => - lot.itemId === scannedItemId && - activeSet.has(lot) && - !isStockOutLineAlreadyProcessed( - processedByItemId, - scannedItemId, - lot.stockOutLineId, - ), - ); - if (candidates.length === 0) return null; - return candidates.find((lot) => isLotRowPending(lot)) ?? candidates[0]; -} - -/** 同物料多行时,优先对「有建议批次号」且未完成的出库行做替换 */ -function pickExpectedLotForSubstitution( - activeSuggestedLots: any[], - processedByItemId?: ProcessedStockOutLinesByItemId, -): any | null { +/** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */ +function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null { if (!activeSuggestedLots?.length) return null; - const itemId = activeSuggestedLots[0]?.itemId; - const processed = processedByItemId ?? new Map(); - const unprocessed = activeSuggestedLots.filter( - (l) => - !itemId || - !isStockOutLineAlreadyProcessed(processed, itemId, l.stockOutLineId), + const withLotNo = activeSuggestedLots.filter( + (l) => l.lotNo != null && String(l.lotNo).trim() !== "" ); - const pool = unprocessed.length > 0 ? unprocessed : activeSuggestedLots; - const withLotNo = pool.filter((l) => l.lotNo != null && String(l.lotNo).trim() !== ""); - const searchPool = withLotNo.length > 0 ? withLotNo : pool; - if (searchPool.length === 1) return searchPool[0]; - const pending = searchPool.find((l) => isLotRowPending(l)); - return pending ?? searchPool[0]; + if (withLotNo.length === 1) return withLotNo[0]; + if (withLotNo.length > 1) { + const pending = withLotNo.find( + (l) => (l.stockOutLineStatus || "").toLowerCase() === "pending" + ); + return pending || withLotNo[0]; + } + return activeSuggestedLots[0]; } const ManualLotConfirmationModal: React.FC<{ @@ -362,11 +237,6 @@ function isInventoryLotLineUnavailable(lot: any): boolean { return String(lot.lotStatus || "").toLowerCase() === "unavailable"; } -/** 過期或不可用:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */ -function isWorkbenchZeroCompleteLot(lot: any): boolean { - return isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot); -} - /** 提貨台「列印標籤」彈窗頂部:依目前表格列判斷可提貨/已用畢/已過期等 */ function isWorkbenchSourceLotExpired(lot: any): boolean { if (!lot) return false; @@ -429,26 +299,18 @@ function getWorkbenchSourceLotStatusSummary(lot: any): { type PickOrderT = (key: string, options?: Record) => string; -function isExpiredWorkbenchReminderMessage(msg: string): boolean { - const trimmed = msg.trim(); - if (!trimmed) return false; - if (/^lot is expired \(expiry=/i.test(trimmed)) return true; - return /已過期/.test(trimmed) || /掃描批號已過期/.test(trimmed); -} +function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string { + const msg = raw.trim(); + if (!msg) return msg; -function buildUnpickableScanRowPatch( - scannedLot: any | null | undefined, - availability: UnpickableScanAvailability, -): Record { - const patch: Record = { lotAvailability: availability }; - if (availability === "status_unavailable") { - patch.lotStatus = "unavailable"; + const expiredMatch = msg.match(/^Lot is expired \(expiry=([^)]+)\)\.?$/i); + if (expiredMatch) { + return t("Lot is expired (expiry={{expiry}})", { + expiry: expiredMatch[1], + }); } - if (scannedLot?.lotNo) patch.lotNo = scannedLot.lotNo; - if (scannedLot?.stockInLineId) patch.stockInLineId = scannedLot.stockInLineId; - if (scannedLot?.expiryDate) patch.expiryDate = scannedLot.expiryDate; - if (scannedLot?.lotId) patch.lotId = scannedLot.lotId; - return patch; + + return t(msg); } /** @@ -522,6 +384,7 @@ const WorkbenchGoodPickExecutionDetail: React.FC = ({ onSwitchToRecordTab, onRefreshReleasedOrderCount, onWorkbenchHierarchyEmpty, + labelPrinter = null, }) => { const workbenchMode = true; const { t } = useTranslation("pickOrder"); @@ -678,10 +541,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const [fgPickOrders, setFgPickOrders] = useState([]); const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); - const isExtraTicket = useMemo( - () => isWorkbenchExtraTicket(fgPickOrders?.[0]?.releaseType, fgPickOrders?.[0]?.ticketNo), - [fgPickOrders], - ); + const isExtraTicket = useMemo(() => { + const ticketNo = String(fgPickOrders?.[0]?.ticketNo ?? "").trim().toUpperCase(); + return ticketNo.startsWith("TI-E-"); + }, [fgPickOrders]); const lotFloorPrefixFilter = useMemo(() => { const storeId = String(fgPickOrders?.[0]?.storeId ?? "") @@ -722,9 +585,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); // Add these missing state variables after line 352 const [isManualScanning, setIsManualScanning] = useState(false); - // Track processed stock-out lines per item (allow same physical lot QR for next SOL) - const [processedQrCombinations, setProcessedQrCombinations] = - useState(new Map()); + // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling + const [processedQrCombinations, setProcessedQrCombinations] = useState>>(new Map()); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); @@ -739,9 +601,6 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); // Use refs for processed QR tracking to avoid useEffect dependency issues and delays const processedQrCodesRef = useRef>(new Set()); const lastProcessedQrRef = useRef(''); - /** Last qrValues.length handled; new pick on same QR requires a new scanner event (length increase). */ - const lastProcessedQrScanCountRef = useRef(0); - const qrPickInFlightRef = useRef(false); // Store callbacks in refs to avoid useEffect dependency issues const processOutsideQrCodeRef = useRef< @@ -902,7 +761,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO ) { workbenchFinishNavigateDoneRef.current = true; const redirectParams = new URLSearchParams(); - redirectParams.set("tab", String(WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE)); + redirectParams.set("tab", "2"); redirectParams.set("ticketNo", ticketForRedirect); if (targetDateForRedirect) { redirectParams.set("targetDate", targetDateForRedirect); @@ -926,7 +785,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO shopName: hierarchicalData.fgInfo.shopName, truckLanceCode: hierarchicalData.fgInfo.truckLanceCode, DepartureTime: hierarchicalData.fgInfo.departureTime, - releaseType: hierarchicalData.fgInfo.releaseType, shopAddress: "", pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", @@ -994,25 +852,21 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO : mergedPickOrder.pickOrderLines || []; pickOrderLinesForDisplay.forEach((line: any) => { - // 用来记录这一行已经通过 lots 出现过的 lotId / stockOutLineId + // 用来记录这一行已经通过 lots 出现过的 lotId const lotIdSet = new Set(); - const stockOutLineIdSet = new Set(); - // ✅ lots:按 SOL 优先去重(其次 lotId),保持 requiredQty 原值,不在前端累计 + // ✅ lots:按 lotId 去重并合并 requiredQty if (line.lots && line.lots.length > 0) { - const lotMap = new Map(); + const lotMap = new Map(); - line.lots.forEach((lot: any, lotIdx: number) => { + line.lots.forEach((lot: any) => { const lotId = lot.id; - const solId = Number(lot?.stockOutLineId); - const lotKey = - Number.isFinite(solId) && solId > 0 - ? `sol:${solId}` - : lotId != null - ? `lot:${lotId}` - : `idx:${lotIdx}`; - if (!lotMap.has(lotKey)) { - lotMap.set(lotKey, { ...lot }); + if (lotMap.has(lotId)) { + const existingLot = lotMap.get(lotId); + existingLot.requiredQty = + (existingLot.requiredQty || 0) + (lot.requiredQty || 0); + } else { + lotMap.set(lotId, { ...lot }); } }); @@ -1020,10 +874,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO if (lot.id != null) { lotIdSet.add(lot.id); } - const solId = Number(lot?.stockOutLineId); - if (Number.isFinite(solId) && solId > 0) { - stockOutLineIdSet.add(solId); - } flatLotData.push({ pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes) @@ -1076,19 +926,20 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO if (line.stockouts && line.stockouts.length > 0) { line.stockouts.forEach((stockout: any) => { const hasLot = stockout.lotId != null; - const hasSolId = stockout.id != null; const lotAlreadyInLots = hasLot && lotIdSet.has(stockout.lotId as number); - const solAlreadyInLots = - hasSolId && stockOutLineIdSet.has(Number(stockout.id)); // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行 - if (!stockout.noLot && (solAlreadyInLots || lotAlreadyInLots)) { + if (!stockout.noLot && lotAlreadyInLots) { return; } - const effectiveStockoutRequiredQty = - stockout?.requiredQty ?? stockout?.suggestedPickLotQty ?? null; + const stockoutRequiredQty = Number( + stockout?.requiredQty ?? stockout?.suggestedPickLotQty, + ); + const effectiveStockoutRequiredQty = Number.isFinite(stockoutRequiredQty) + ? stockoutRequiredQty + : Number(line.requiredQty) || 0; const fallbackRouteFromLine = line?.lots?.[0]?.router?.route ?? line?.lots?.[0]?.location ?? null; @@ -1196,64 +1047,12 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO } else { setWorkbenchLotLabelInitialPayload(null); } - setWorkbenchLotLabelReminderText( - reminderText ? translateWorkbenchRejectMessage(reminderText, t) : null, - ); + setWorkbenchLotLabelReminderText(reminderText ?? null); // Clear latched success so the lot-label modal effect cannot instantly re-close on open. setQrScanSuccess(false); setWorkbenchLotLabelModalOpen(true); }, - [t], - ); - - /** Patch pick row locally so table shows 已過期/不可用 without full refresh. */ - const patchWorkbenchRowForUnpickableScan = useCallback( - ( - pickRow: any, - scannedLot: any | null | undefined, - availability: UnpickableScanAvailability, - ) => { - const solId = Number(pickRow?.stockOutLineId); - if (!solId) return; - const rowPatch = buildUnpickableScanRowPatch(scannedLot, availability); - const mapRows = (prev: any[]) => - prev.map((lot) => - Number(lot.stockOutLineId) === solId ? { ...lot, ...rowPatch } : lot, - ); - setCombinedLotData(mapRows); - setOriginalCombinedData(mapRows); - clearWorkbenchScanReject(solId); - }, - [clearWorkbenchScanReject], - ); - - const openUnpickableScanLotLabelModal = useCallback( - ( - pickRow: any, - scannedLot: any | null | undefined, - reminderText: string, - ) => { - const fromMsg = inferUnpickableScanAvailability(reminderText); - const availability = - fromMsg ?? - (isWorkbenchSourceLotExpired(scannedLot ?? pickRow) - ? "expired" - : isInventoryLotLineUnavailable(scannedLot ?? pickRow) - ? "status_unavailable" - : null); - const mergedPickRow = - availability != null - ? { ...pickRow, ...buildUnpickableScanRowPatch(scannedLot, availability) } - : pickRow; - if (availability != null) { - patchWorkbenchRowForUnpickableScan(pickRow, scannedLot, availability); - } - openWorkbenchLotLabelModalForLot(mergedPickRow, reminderText); - }, - [ - patchWorkbenchRowForUnpickableScan, - openWorkbenchLotLabelModalForLot, - ], + [], ); const shouldOpenWorkbenchLotLabelModalForFailure = useCallback( @@ -1338,17 +1137,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO severity: undefined as "success" | "warning" | "error" | undefined, }; } - const reminder = workbenchLotLabelReminderText?.trim() ?? ""; - if (reminder && isExpiredWorkbenchReminderMessage(reminder)) { - return { text: "此批號狀態:已過期", severity: "error" as const }; - } const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot); return { text: s.text, severity: s.severity }; - }, [ - workbenchLotLabelModalOpen, - workbenchLotLabelContextLot, - workbenchLotLabelReminderText, - ]); + }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot]); const workbenchLotLabelSubmitQty = useMemo(() => { if (!workbenchLotLabelContextLot) return 0; @@ -1399,7 +1190,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO const event = new CustomEvent('pickOrderCompletionStatus', { detail: { allLotsCompleted, - tabIndex: WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE, + tabIndex: 2 // DO workbench「Finished Good Record (mine)」分頁索引 } }); window.dispatchEvent(event); @@ -1613,36 +1404,16 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO resetScanRef.current = resetScan; const processOutsideQrCode = useCallback(async (latestQr: string, qrScanCountAtInvoke?: number) => { - if (qrPickInFlightRef.current) { - console.log(" [SKIP] QR pick already in flight"); - return; - } - qrPickInFlightRef.current = true; - const totalStartTime = performance.now(); console.log(` [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); console.log(` Start time: ${new Date().toISOString()}`); - + + // ✅ Measure index access time const indexAccessStart = performance.now(); - const indexes = lotDataIndexes; + const indexes = lotDataIndexes; // Access the memoized indexes const indexAccessTime = performance.now() - indexAccessStart; console.log(` [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`); - - const recordHandledQrScanCount = (scanCount: number | undefined) => { - if (scanCount != null && scanCount > lastProcessedQrScanCountRef.current) { - lastProcessedQrScanCountRef.current = scanCount; - } - }; - - /** - * Stop QR effect re-entry after unpickable modal (expired/unavailable API fail). - * Do NOT mark stock-out line as processed — pick was not completed; user may scan another lot. - */ - const markUnpickableScanSessionHandled = () => { - recordHandledQrScanCount(qrScanCountAtInvoke); - }; - - try { + // 1) Parse JSON safely (parse once, reuse) const parseStartTime = performance.now(); let qrData: any = null; @@ -1675,33 +1446,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO const scannedItemId = qrData.itemId; const scannedStockInLineId = qrData.stockInLineId; - - const hasPendingOnScannedSil = hasPendingUnprocessedRowForStockInLine( - indexes, - scannedItemId, - scannedStockInLineId, - processedQrCombinations, - ); - const hasPendingOnItem = hasPendingUnprocessedRowForItem( - indexes, - scannedItemId, - processedQrCombinations, - ); - if (!hasPendingOnScannedSil && !hasPendingOnItem) { - console.log( - ` [SKIP] No pending stock-out line left for itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId}`, - ); - startTransition(() => { - setQrScanError(true); - setQrScanSuccess(false); - setQrScanErrorMsg( - t( - "No pending pick line left for this item. It may already be completed or fully processed.", - ), - ); - }); + + // ✅ Check if this combination was already processed + const duplicateCheckStartTime = performance.now(); + const itemProcessedSet = processedQrCombinations.get(scannedItemId); + if (itemProcessedSet?.has(scannedStockInLineId)) { + const duplicateCheckTime = performance.now() - duplicateCheckStartTime; + console.log(` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`); return; } + const duplicateCheckTime = performance.now() - duplicateCheckStartTime; + console.log(` [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`); // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) const lookupStartTime = performance.now(); @@ -1713,15 +1468,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots // This allows users to scan other lots even when all suggested lots are rejected - const stockInLineRows = indexes.byStockInLineId.get(scannedStockInLineId) ?? []; - const scannedLot = - findExactActiveMatchForStockInLine( - stockInLineRows, - scannedItemId, - activeSuggestedLots, - processedQrCombinations, - ) ?? - allLotsForItem.find((lot: any) => lot.stockInLineId === scannedStockInLineId); + const scannedLot = allLotsForItem.find( + (lot: any) => lot.stockInLineId === scannedStockInLineId + ); if (scannedLot) { const isRejected = @@ -1738,7 +1487,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` ); }); - markUnpickableScanSessionHandled(); + // Mark as processed to prevent re-processing + setProcessedQrCombinations(prev => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); return; } @@ -1748,27 +1503,25 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO setQrScanError(false); setQrScanSuccess(false); }); - openUnpickableScanLotLabelModal( - scannedLot, + openWorkbenchLotLabelModalForLot( scannedLot, t("This lot is not available, please scan another lot."), ); - markUnpickableScanSessionHandled(); return; } - if (isWorkbenchSourceLotExpired(scannedLot)) { + const isExpired = + String(scannedLot.lotAvailability || '').toLowerCase() === 'expired'; + if (isExpired) { console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is expired; opening lot-label modal`); startTransition(() => { setQrScanError(false); setQrScanSuccess(false); }); - openUnpickableScanLotLabelModal( - scannedLot, + openWorkbenchLotLabelModalForLot( scannedLot, `Lot is expired (expiry=${scannedLot.expiryDate || "-"})`, ); - markUnpickableScanSessionHandled(); return; } } @@ -1871,16 +1624,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && expectedLot ) { - openUnpickableScanLotLabelModal( - expectedLot, - scannedLot ?? - allLotsForItem.find( - (lot: any) => lot.stockInLineId === scannedStockInLineId, - ) ?? - null, - failMsg, - ); - markUnpickableScanSessionHandled(); + openWorkbenchLotLabelModalForLot(expectedLot, failMsg); return; } if (workbenchMode && expectedLot.stockOutLineId != null) { @@ -1936,13 +1680,12 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO ); }); - const nextProcessedNoActive = markProcessedStockOutLine( - processedQrCombinations, - scannedItemId, - expectedLot.stockOutLineId, - ); - setProcessedQrCombinations(nextProcessedNoActive); - recordHandledQrScanCount(qrScanCountAtInvoke); + setProcessedQrCombinations((prev) => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); if (workbenchMode) { await refreshWorkbenchAfterScanPick(); @@ -1953,13 +1696,16 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1)) const matchStartTime = performance.now(); + let exactMatch: any = null; const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || []; - const exactMatch = findExactActiveMatchForStockInLine( - stockInLineLots, - scannedItemId, - activeSuggestedLots, - processedQrCombinations, - ); + // Find exact match from stockInLineId index, then verify it's in active lots + for (let i = 0; i < stockInLineLots.length; i++) { + const lot = stockInLineLots[i]; + if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) { + exactMatch = lot; + break; + } + } const matchTime = performance.now() - matchStartTime; console.log(` [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); @@ -1968,8 +1714,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) if (!exactMatch) { const expectedLot = - pickExpectedLotForSubstitution(activeSuggestedLots, processedQrCombinations) || - allLotsForItem[0]; + pickExpectedLotForSubstitution(activeSuggestedLots) || allLotsForItem[0]; if (expectedLot) { const shouldAutoSwitch = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); @@ -2046,18 +1791,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && expectedLot ) { - openUnpickableScanLotLabelModal( - expectedLot, - scannedLot ?? - allLotsForItem.find( - (lot: any) => lot.stockInLineId === scannedStockInLineId, - ) ?? { - stockInLineId: scannedStockInLineId, - lotNo: scannedLotNo, - }, - failMsg, - ); - markUnpickableScanSessionHandled(); + openWorkbenchLotLabelModalForLot(expectedLot, failMsg); return; } if (workbenchMode && expectedLot.stockOutLineId != null) { @@ -2113,13 +1847,12 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO ); }); - const nextProcessedAuto = markProcessedStockOutLine( - processedQrCombinations, - scannedItemId, - expectedLot.stockOutLineId, - ); - setProcessedQrCombinations(nextProcessedAuto); - recordHandledQrScanCount(qrScanCountAtInvoke); + setProcessedQrCombinations((prev) => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); if (workbenchMode) { await refreshWorkbenchAfterScanPick(); } @@ -2212,13 +1945,14 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // Mark this combination as processed const markProcessedStartTime = performance.now(); - const nextProcessedExact = markProcessedStockOutLine( - processedQrCombinations, - scannedItemId, - exactMatch.stockOutLineId, - ); - setProcessedQrCombinations(nextProcessedExact); - recordHandledQrScanCount(qrScanCountAtInvoke); + setProcessedQrCombinations(prev => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) { + newMap.set(scannedItemId, new Set()); + } + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); const markProcessedTime = performance.now() - markProcessedStartTime; console.log(` [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`); @@ -2229,7 +1963,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO const totalTime = performance.now() - totalStartTime; console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); console.log(` End time: ${new Date().toISOString()}`); - console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`); + console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`); console.log( workbenchMode ? "✅ Workbench scan-pick: list refreshed from server" @@ -2244,8 +1978,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && exactMatch ) { - openUnpickableScanLotLabelModal(exactMatch, exactMatch, failMsg); - markUnpickableScanSessionHandled(); + openWorkbenchLotLabelModalForLot(exactMatch, failMsg); return; } if (workbenchMode && exactMatch.stockOutLineId != null) { @@ -2272,11 +2005,20 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 + const mismatchCheckStartTime = performance.now(); + const itemProcessedSet2 = processedQrCombinations.get(scannedItemId); + if (itemProcessedSet2?.has(scannedStockInLineId)) { + const mismatchCheckTime = performance.now() - mismatchCheckStartTime; + console.log( + ` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`, + ); + return; + } + const mismatchCheckTime = performance.now() - mismatchCheckStartTime; + console.log(` [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`); + const expectedLotStartTime = performance.now(); - const expectedLot = pickExpectedLotForSubstitution( - activeSuggestedLots, - processedQrCombinations, - ); + const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots); if (!expectedLot) { console.error("Could not determine expected lot for auto-switch"); startTransition(() => { @@ -2359,18 +2101,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && expectedLot ) { - openUnpickableScanLotLabelModal( - expectedLot, - scannedLot ?? - allLotsForItem.find( - (lot: any) => lot.stockInLineId === scannedStockInLineId, - ) ?? { - stockInLineId: scannedStockInLineId, - lotNo: scannedLotNo, - }, - failMsg, - ); - markUnpickableScanSessionHandled(); + openWorkbenchLotLabelModalForLot(expectedLot, failMsg); return; } if (workbenchMode && expectedLot.stockOutLineId != null) { @@ -2426,13 +2157,12 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO ); }); - const nextProcessedMismatch = markProcessedStockOutLine( - processedQrCombinations, - scannedItemId, - expectedLot.stockOutLineId, - ); - setProcessedQrCombinations(nextProcessedMismatch); - recordHandledQrScanCount(qrScanCountAtInvoke); + setProcessedQrCombinations((prev) => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); if (workbenchMode) { await refreshWorkbenchAfterScanPick(); @@ -2450,7 +2180,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO ); console.log(` End time: ${new Date().toISOString()}`); console.log( - `📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms`, + `📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms`, ); } catch (error) { const totalTime = performance.now() - totalStartTime; @@ -2462,9 +2192,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO }); return; } - } finally { - qrPickInFlightRef.current = false; - } }, [ lotDataIndexes, processedQrCombinations, @@ -2477,7 +2204,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO refreshWorkbenchAfterScanPick, workbenchScanPickQtyFromLot, openWorkbenchLotLabelModalForLot, - openUnpickableScanLotLabelModal, shouldOpenWorkbenchLotLabelModalForFailure, t, ]); @@ -2583,37 +2309,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // If it's a different QR, allow processing console.log(` [QR PROCESS] Different QR detected while manual modal open, allowing processing`); } - - // Skip re-processing while lot-label modal is already open for this scan - if (workbenchLotLabelModalOpen && latestQr === lastProcessedQrRef.current) { - console.log(` [QR PROCESS] Skipping - lot-label modal open for same QR`); - return; - } const qrDetectionStartTime = performance.now(); console.log(` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); - const scanCount = qrValues.length; - const qrPayload = parseWorkbenchQrPayload(latestQr); - const isNewScanEvent = scanCount > lastProcessedQrScanCountRef.current; - const canRetrySamePhysicalLot = - qrPayload != null && - isNewScanEvent && - hasPendingUnprocessedRowForStockInLine( - lotDataIndexes, - qrPayload.itemId, - qrPayload.stockInLineId, - processedQrCombinations, - ); - - // Skip if already processed; same QR only retries on a new scanner event (qrValues.length increase) + // Skip if already processed (use refs to avoid dependency issues and delays) const checkProcessedStartTime = performance.now(); - if ( - (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) && - !canRetrySamePhysicalLot - ) { + if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) { const checkTime = performance.now() - checkProcessedStartTime; console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); return; @@ -2652,7 +2356,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO } // Process new QR code immediately (background mode - no modal) - if (latestQr && (latestQr !== lastProcessedQrRef.current || isNewScanEvent)) { + // Check against refs to avoid state update delays + if (latestQr && latestQr !== lastProcessedQrRef.current) { const processingStartTime = performance.now(); console.log(` [QR PROCESS] Starting processing at: ${new Date().toISOString()}`); console.log(` [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`); @@ -2716,16 +2421,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO qrProcessingTimeoutRef.current = null; } }; - }, [ - qrValues, - isManualScanning, - isRefreshingData, - combinedLotData.length, - manualLotConfirmationOpen, - workbenchLotLabelModalOpen, - lotDataIndexes, - processedQrCombinations, - ]); + }, [qrValues, isManualScanning, isRefreshingData, combinedLotData.length, manualLotConfirmationOpen]); const renderCountRef = useRef(0); const renderStartTimeRef = useRef(null); @@ -2951,9 +2647,7 @@ useEffect(() => { type RowMeta = { lot: any; isGroupFirst: boolean; - isGroupLast: boolean; groupDisplayIndex: number; - isMultiLotGroup: boolean; }; const isCompletedStatus = (lot: any) => { @@ -2987,14 +2681,15 @@ useEffect(() => { { firstIndex: number; items: { lot: any; originalIndex: number }[] } >(); combinedLotData.forEach((lot: any, originalIndex: number) => { + const routeKey = String(lot?.routerRoute ?? "").trim(); const pickOrderLineKey = lot?.pickOrderLineId != null ? `pol:${String(lot.pickOrderLineId)}` : "pol:unknown"; const itemKey = lot?.itemId != null ? `itemId:${String(lot.itemId)}` : `itemCode:${String(lot?.itemCode ?? "").trim()}`; - // Group by pick order line + item so split lots (different routes) share one display index. - const key = `${pickOrderLineKey}__${itemKey}`; + // Group by pickOrderLine first so no-lot row stays with its lot rows even when route is empty. + const key = `${pickOrderLineKey}__${itemKey}__${routeKey}`; const g = groups.get(key); if (!g) { groups.set(key, { firstIndex: originalIndex, items: [{ lot, originalIndex }] }); @@ -3022,9 +2717,7 @@ useEffect(() => { flattened.push({ lot: it.lot, isGroupFirst: idx === 0, - isGroupLast: idx === sortedWithin.length - 1, groupDisplayIndex, - isMultiLotGroup: sortedWithin.length > 1, }); }); } @@ -3070,8 +2763,8 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe try { if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); - const targetZeroComplete = isWorkbenchZeroCompleteLot(lot); - const effectiveSubmitQty = targetZeroComplete && submitQty > 0 ? 0 : submitQty; + const targetUnavailable = isInventoryLotLineUnavailable(lot); + const effectiveSubmitQty = targetUnavailable && submitQty > 0 ? 0 : submitQty; const canonicalLotForSol = solId > 0 @@ -3096,10 +2789,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe const qtyPayload = workbenchScanPickQtyFromLot(canonicalLotForSol); const wbJustQty = qtyPayload.qty; - const isZeroCompleteForJustComplete = isWorkbenchZeroCompleteLot(canonicalLotForSol); + const isUnavailableForJustComplete = isInventoryLotLineUnavailable(canonicalLotForSol); const canPostScanPick = - // expired / unavailable: Just Completed always submits qty=0, even without lotNo - isZeroCompleteForJustComplete || ( + // unavailable lot: Just Completed must always submit qty=0, even without lotNo + isUnavailableForJustComplete || ( canonicalLotForSol.lotNo && String(canonicalLotForSol.lotNo).trim() !== "" && ( // explicit short submit: user typed 0 (must send qty=0 to backend) (hasExplicitSubmitOverride && @@ -3111,7 +2804,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe ); if (canPostScanPick) { - const qtyToSend = isZeroCompleteForJustComplete + const qtyToSend = isUnavailableForJustComplete ? 0 : hasExplicitSubmitOverride && explicitSubmitOverride === 0 ? 0 @@ -3156,7 +2849,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe return; } const justCompleteErr = t( - "Just Completed (workbench): requires a valid lot number and quantity.", + "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.", ); if (solId > 0) { rememberWorkbenchScanReject(solId, justCompleteErr); @@ -3643,12 +3336,12 @@ const handleSubmitAllScanned = useCallback(async () => { // 添加调试日志 const noLotCount = filtered.filter(l => l.noLot === true).length; const normalCount = filtered.filter(l => l.noLot !== true).length; - //console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`); - //console.log(`📊 All items breakdown:`, { - //total: combinedLotData.length, - //noLot: combinedLotData.filter(l => l.noLot === true).length, - //normal: combinedLotData.filter(l => l.noLot !== true).length - //}); + console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`); + console.log(`📊 All items breakdown:`, { + total: combinedLotData.length, + noLot: combinedLotData.filter(l => l.noLot === true).length, + normal: combinedLotData.filter(l => l.noLot !== true).length + }); return filtered.length; }, [combinedLotData]); @@ -3674,6 +3367,77 @@ const handleSubmitAllScanned = useCallback(async () => { }; }, [isManualScanning, stopScan, resetScan]); + const blankLabelPrintInFlightRef = useRef(false); + const [isPrintingBlankLabels, setIsPrintingBlankLabels] = useState(false); + const workbenchDoPickOrderId = fgPickOrders[0]?.doPickOrderId; + + const askBlankLabelQuantity = useCallback(async (): Promise => { + const result = await Swal.fire({ + title: t("Enter blank label print quantity:"), + icon: "info", + input: "number", + inputPlaceholder: t("Label print quantity"), + inputAttributes: { + min: "1", + step: "1", + }, + inputValidator: (value) => { + if (!value) return t("You need to enter a number"); + if (parseInt(value, 10) < 1) return t("Number must be at least 1"); + return null; + }, + showCancelButton: true, + confirmButtonText: t("Confirm"), + cancelButtonText: t("Cancel"), + confirmButtonColor: "#8dba00", + cancelButtonColor: "#F04438", + }); + if (!result.isConfirmed) return null; + return parseInt(result.value, 10); + }, [t]); + + const handlePrintBlankPageLabels = useCallback(async () => { + if (blankLabelPrintInFlightRef.current) return; + if (!labelPrinter) { + await Swal.fire({ + position: "bottom-end", + icon: "warning", + text: t("Please select a label printer first"), + showConfirmButton: false, + timer: 1500, + }); + return; + } + if (!workbenchDoPickOrderId) return; + + const labelPrintQty = await askBlankLabelQuantity(); + if (!labelPrintQty) return; + + blankLabelPrintInFlightRef.current = true; + setIsPrintingBlankLabels(true); + try { + const response = await printDNLabelsWorkbench({ + deliveryOrderPickOrderId: workbenchDoPickOrderId, + printerId: labelPrinter.id, + printQty: 1, + numOfCarton: labelPrintQty, + blankCartonNumber: true, + }); + if (response.success) { + await Swal.fire({ + position: "bottom-end", + icon: "success", + text: t("Printed Successfully."), + showConfirmButton: false, + timer: 1500, + }); + } + } finally { + setIsPrintingBlankLabels(false); + blankLabelPrintInFlightRef.current = false; + } + }, [askBlankLabelQuantity, labelPrinter, t, workbenchDoPickOrderId]); + const getStatusMessage = useCallback((lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': @@ -3718,11 +3482,30 @@ const handleSubmitAllScanned = useCallback(async () => { boxShadow: 'none', }} > - + + + + + + { {t("Ticket No.")}: {fgPickOrders[0].ticketNo || '-'}{isExtraTicket ? ` (${t("isExtra order")})` : ""} - {t("Departure Time")}: {formatDepartureTime(fgPickOrders[0].DepartureTime)} + {t("Departure Time")}: {fgPickOrders[0].DepartureTime || '-'} @@ -3945,7 +3728,7 @@ paginatedData.map((row, index) => { const solIdForKey = Number(lot.stockOutLineId) || 0; const lotKeyForSubmitQty = Number.isFinite(solIdForKey) && solIdForKey > 0 ? `sol:${solIdForKey}` : `${lot.pickOrderLineId}-${lot.lotId}`; - const lockedSubmitQtyDisplay = isWorkbenchZeroCompleteLot(lot) ? 0 : resolveSingleSubmitQty(lot); + const lockedSubmitQtyDisplay = isInventoryLotLineUnavailable(lot) ? 0 : resolveSingleSubmitQty(lot); const hasPickOverride = Object.prototype.hasOwnProperty.call(pickQtyData, lotKeyForSubmitQty); const fromPickRow = hasPickOverride ? pickQtyData[lotKeyForSubmitQty] : undefined; const workbenchSubmitQtyDisplay = @@ -3959,42 +3742,16 @@ paginatedData.map((row, index) => { const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); const isSolRejected = solSt === "rejected" || String(lot.lotAvailability || "").toLowerCase() === "rejected"; - const isFirstInGroup = row.isGroupFirst; - const isLastInGroup = row.isGroupLast; - const shouldOutline = row.isMultiLotGroup; - const outlineColor = "#008000"; - const groupUsesAltBg = row.groupDisplayIndex % 2 === 0; - const groupRowBg = groupUsesAltBg ? "neutral.50" : "background.paper"; + return ( @@ -4009,7 +3766,7 @@ paginatedData.map((row, index) => { - {lot.noLot ? "-" : lot.routerRoute || "-"} + {lot.routerRoute || '-'} @@ -4084,13 +3841,8 @@ paginatedData.map((row, index) => { {(() => { - if (!row.isGroupFirst) return null; - const requiredQty = lot.pickOrderLineRequiredQty; - if (requiredQty == null) return null; - const requiredQtyNum = Number(requiredQty); - if (!Number.isFinite(requiredQtyNum)) return String(requiredQty); - const uom = lot.uomShortDesc ? `(${lot.uomShortDesc})` : ""; - return requiredQtyNum.toLocaleString() + uom; + const requiredQty = lot.requiredQty || 0; + return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')'; })()} @@ -4191,6 +3943,7 @@ paginatedData.map((row, index) => { const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; const isNoLot = !lot.lotNo; const isUnavailableRow = isInventoryLotLineUnavailable(lot); + // ✅ rejected lot:显示提示文本(换行显示) if (isRejected && !isNoLot) { const rejectHint = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t); @@ -4250,103 +4003,103 @@ paginatedData.map((row, index) => { ? String(pickQtyData[lotKey]) : String(displayedSubmitQty) : String(displayedSubmitQty); - const isRowPicked = - status === "completed" || - status === "checked" || - status === "partially_completed" || - status === "partially_complete"; return ( - - {isRowPicked ? ( - - {textFieldValue} - - ) : ( - { - if (!qtyFieldEnabled) return; - if (e.key !== "{") return; - e.preventDefault(); - setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ - ...prev, - [lotKey]: false, - })); - (e.currentTarget as HTMLInputElement).blur(); - }} - onChange={(e) => { - if (!qtyFieldEnabled) return; - const n = Number(e.target.value); - if (Number.isFinite(n) && n < 0) return; - handlePickQtyChange(lotKey, e.target.value); - }} - inputProps={{ min: 0, step: 1 }} - sx={{ - width: 96, - "& .MuiInputBase-input": { fontSize: "0.75rem", py: 0.5, textAlign: "center" }, - }} - /> - )} + + {/* + + */} - {isRowPicked ? ( - - {t("")} - - ) : ( - <> - - - - )} + { + if (!qtyFieldEnabled) return; + if (e.key !== "{") return; + e.preventDefault(); + setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ + ...prev, + [lotKey]: false, + })); + (e.currentTarget as HTMLInputElement).blur(); + }} + onChange={(e) => { + if (!qtyFieldEnabled) return; + const n = Number(e.target.value); + if (Number.isFinite(n) && n < 0) return; + handlePickQtyChange(lotKey, e.target.value); + }} + inputProps={{ min: 0, step: 1 }} + sx={{ + width: 96, + '& .MuiInputBase-input': { fontSize: '0.75rem', py: 0.5, textAlign: 'center' }, + }} + /> + + + ); } diff --git a/src/components/UserSearch/UserExcelSheetView.tsx b/src/components/UserSearch/UserExcelSheetView.tsx index 858d794..23107c3 100644 --- a/src/components/UserSearch/UserExcelSheetView.tsx +++ b/src/components/UserSearch/UserExcelSheetView.tsx @@ -1,8 +1,9 @@ "use client"; -import { memo, useCallback, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, + Button, Checkbox, IconButton, Paper, @@ -48,6 +49,31 @@ function hasUserAuthority(user: UserListDetail, authorityId: number): boolean { ); } +function cloneUserList(list: UserListDetail[]): UserListDetail[] { + return list.map(user => ({ + ...user, + authIds: [...(user.authIds ?? [])], + auths: (user.auths ?? []).map(auth => ({ ...auth })), + })); +} + +function computeAuthorityChanges( + baseline: UserListDetail, + current: UserListDetail, + authorityIds: number[], +): { addAuthIds: number[]; removeAuthIds: number[] } { + const addAuthIds: number[] = []; + const removeAuthIds: number[] = []; + for (const authorityId of authorityIds) { + const wasChecked = hasUserAuthority(baseline, authorityId); + const isChecked = hasUserAuthority(current, authorityId); + if (wasChecked === isChecked) continue; + if (isChecked) addAuthIds.push(authorityId); + else removeAuthIds.push(authorityId); + } + return { addAuthIds, removeAuthIds }; +} + type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; type SearchBoxQuery = Record; @@ -67,44 +93,102 @@ const bodyCellSx = { whiteSpace: "nowrap", }; +const changedRowBg = "#fff8e1"; +const changedCellOutline = "#f9a825"; + +const checkboxSx = { + p: 0.5, + "&:hover": { backgroundColor: "transparent" }, + "&.Mui-focusVisible": { backgroundColor: "transparent" }, +}; + +function buildChangeHighlights( + allUsers: UserListDetail[], + savedUsers: UserListDetail[], + authorityIds: number[], +) { + const changedUserIds = new Set(); + const changedAuthorityKeys = new Set(); + + if (authorityIds.length === 0) { + return { changedUserIds, changedAuthorityKeys }; + } + + for (const user of allUsers) { + const baseline = savedUsers.find(item => item.id === user.id); + if (!baseline) continue; + + for (const authorityId of authorityIds) { + if (hasUserAuthority(baseline, authorityId) === hasUserAuthority(user, authorityId)) { + continue; + } + changedUserIds.add(user.id); + changedAuthorityKeys.add(`${user.id}-${authorityId}`); + } + } + + return { changedUserIds, changedAuthorityKeys }; +} + /** Memoized so toggling one checkbox does not re-render every cell on the page. */ const AuthorityCheckboxCell = memo(function AuthorityCheckboxCell({ checked, + changed, + rowChanged, disabled, userId, authorityId, onToggle, }: { checked: boolean; + changed: boolean; + rowChanged: boolean; disabled: boolean; userId: number; authorityId: number; onToggle: (userId: number, authorityId: number, checked: boolean) => void; }) { return ( - + onToggle(userId, authorityId, next)} + disableRipple + disableFocusRipple + onChange={(e, next) => { + onToggle(userId, authorityId, next); + (e.target as HTMLInputElement).blur(); + }} + sx={checkboxSx} /> ); }); const UserExcelSheetView: React.FC = ({ users }) => { - const { t } = useTranslation("user"); + const { t } = useTranslation(["user", "common"]); const router = useRouter(); - const [allUsers, setAllUsers] = useState(users); - const allUsersRef = useRef(allUsers); - allUsersRef.current = allUsers; + const [savedUsers, setSavedUsers] = useState(() => cloneUserList(users)); + const [allUsers, setAllUsers] = useState(() => cloneUserList(users)); const [searchQuery, setSearchQuery] = useState({}); - const [updatingKey, setUpdatingKey] = useState(null); + const [isSaving, setIsSaving] = useState(false); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(20); - /** Prevents double-submit on the same checkbox; other cells stay clickable. */ - const inFlightKeysRef = useRef(new Set()); + const saveInFlightRef = useRef(false); + + useEffect(() => { + const next = cloneUserList(users); + setSavedUsers(next); + setAllUsers(next); + }, [users]); const searchCriteria: Criterion[] = useMemo( () => [ @@ -150,6 +234,18 @@ const UserExcelSheetView: React.FC = ({ users }) => { return filteredUsers.slice(start, start + rowsPerPage); }, [filteredUsers, page, rowsPerPage]); + const authorityIds = useMemo( + () => authorityColumns.map(authority => authority.id), + [authorityColumns], + ); + + const { changedUserIds, changedAuthorityKeys } = useMemo( + () => buildChangeHighlights(allUsers, savedUsers, authorityIds), + [allUsers, savedUsers, authorityIds], + ); + + const hasPendingChanges = changedUserIds.size > 0; + const handleEdit = useCallback( (user: UserResult) => { router.push(`/settings/user/edit?id=${user.id}`); @@ -170,50 +266,74 @@ const UserExcelSheetView: React.FC = ({ users }) => { ); const handleAuthorityToggle = useCallback( - async (userId: number, authorityId: number, checked: boolean) => { - const user = allUsersRef.current.find(u => u.id === userId); - if (!user || hasUserAuthority(user, authorityId) === checked) return; - - const key = `${userId}-${authorityId}`; - if (inFlightKeysRef.current.has(key)) return; - inFlightKeysRef.current.add(key); - - const updateList = (list: UserListDetail[], nextChecked: boolean) => - list.map(item => + (userId: number, authorityId: number, checked: boolean) => { + if (isSaving) return; + setAllUsers(prev => + prev.map(item => item.id !== userId ? item : { ...item, auths: (item.auths ?? []).map(auth => - auth.id === authorityId ? { ...auth, v: nextChecked ? 1 : 0 } : auth, + auth.id === authorityId ? { ...auth, v: checked ? 1 : 0 } : auth, ), - authIds: nextChecked + authIds: checked ? Array.from(new Set([...(item.authIds ?? []), authorityId])) : (item.authIds ?? []).filter(id => id !== authorityId), }, + ), + ); + }, + [isSaving], + ); + + const handleSave = useCallback(async () => { + if (!hasPendingChanges || saveInFlightRef.current) return; + saveInFlightRef.current = true; + setIsSaving(true); + try { + const usersToUpdate = allUsers.filter(user => { + const baseline = savedUsers.find(item => item.id === user.id); + if (!baseline) return false; + const { addAuthIds, removeAuthIds } = computeAuthorityChanges( + baseline, + user, + authorityIds, ); + return addAuthIds.length > 0 || removeAuthIds.length > 0; + }); - setAllUsers(prev => updateList(prev, checked)); - setUpdatingKey(key); - try { - await updateUser(userId, { + for (const user of usersToUpdate) { + const baseline = savedUsers.find(item => item.id === user.id)!; + const { addAuthIds, removeAuthIds } = computeAuthorityChanges( + baseline, + user, + authorityIds, + ); + await updateUser(user.id, { username: user.username, name: user.name, staffNo: user.staffNo?.toString(), locked: false, - addAuthIds: checked ? [authorityId] : [], - removeAuthIds: checked ? [] : [authorityId], + addAuthIds, + removeAuthIds, }); - } catch (error) { - console.error("Failed to update authority", error); - setAllUsers(prev => updateList(prev, !checked)); - } finally { - setUpdatingKey(null); - inFlightKeysRef.current.delete(key); } - }, - [], - ); + + const snapshot = cloneUserList(allUsers); + setSavedUsers(snapshot); + setAllUsers(snapshot); + router.refresh(); + await successDialog(t("Update Success", { ns: "common" }), t); + } catch (error) { + console.error("Failed to save user authorities", error); + setAllUsers(cloneUserList(savedUsers)); + alert(t("Save failed. Please try again.", { defaultValue: "儲存失敗,請再試一次。" })); + } finally { + setIsSaving(false); + saveInFlightRef.current = false; + } + }, [allUsers, authorityIds, hasPendingChanges, router, savedUsers, t]); return ( <> @@ -225,6 +345,18 @@ const UserExcelSheetView: React.FC = ({ users }) => { }} /> + {hasPendingChanges && ( + + + + )} + @@ -249,9 +381,22 @@ const UserExcelSheetView: React.FC = ({ users }) => { {pagedUsers.length > 0 ? ( - pagedUsers.map((user, index) => ( - - + pagedUsers.map((user, index) => { + const rowChanged = changedUserIds.has(user.id); + return ( + + handleEdit(user)}> @@ -280,17 +425,26 @@ const UserExcelSheetView: React.FC = ({ users }) => { userId={user.id} authorityId={authority.id} checked={hasUserAuthority(user, authority.id)} - disabled={updatingKey === `${user.id}-${authority.id}`} + changed={changedAuthorityKeys.has(`${user.id}-${authority.id}`)} + rowChanged={rowChanged} + disabled={isSaving} onToggle={handleAuthorityToggle} /> ))} - + handleDelete(user)}> - )) + ); + }) ) : ( diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index ca353ab..a620b4a 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -1,558 +1,535 @@ -{ - "Purchase Order": "採購訂單", - "Code": "編號", - "Pick Order Code": "提料單編號", - "Item Code": "貨品編號", - "OrderDate": "下單日期", - "Details": "詳情", - "Supplier": "供應商", - "Status": "來貨狀態", - "N/A": "不適用", - "Release Pick Orders": "放單", - "released": "已放單", - "consolidated": "已合併", - "assigned": "已分派", - "picking": "提料中", - "Loading...": "加載中", - "Suggestion success": "建議成功", - "Scan pick success": "掃描提料成功", - "Remark": "備註", - "Available Qty": "可用數量", - "Picked Qty": "已提料數量", - "Escalated": "上報狀態", - "NotEscalated": "無上報", - "Assigned To": "已分配", - "Progress": "進度", - "Select Remark": "選擇備註", - "Just Complete": "已完成", - "Skip": "跳過", - "if need just edit number, please scan the lot again": "如果需要只修改數量,請重新掃描批次。", - "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}": "總數量(實際提料 + 遺失 + 不良)不能超過可用數量:{{available}}", - "Confirm Assignment": "確認分配", - "Assigned successfully": "分派成功", - "Assignment failed": "分配失敗", - "Required Date": "所需日期", - "Store": "位置", - "Available Orders": "可用訂單", - "This lot is rejected, please scan another lot.": "此批次已拒收,請掃描另一個批次。", - "Lane Code": "車線號碼", - "Fetching all matching records...": "正在獲取所有匹配的記錄...", - "Edit": "改數", - "Submit Qty": "提交數量", - "Just Completed": "已完成", - "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.": "已完成(工作台):需有效批號與可提交數量;過期列請勿使用此按鈕。", - "Just Completed (workbench): requires a valid lot number and quantity.": "已完成(工作台):需有效批號與可提交數量。", - "Do you want to start?": "確定開始嗎?", - "Start": "開始", - "Pick Order Code(s)": "提料單編號", - "Delivery Order Code(s)": "提料單編號", - "Start Success": "開始成功", - "Qty will submit": "提交數量", - "Truck Lance Code": "車線號碼", - "Pick Order Codes": "提料單編號", - "Pick Order Lines": "提料單行數", - "Delivery Order Codes": "提料單編號", - "Delivery Order Lines": "送貨單行數", - "Lines Per Pick Order": "每提料單行數", - "Pick Orders Details": "提料單詳情", - "Lines": "行數", - "Before Today": "以前", - "Truck X": "車線-X", - "Finsihed good items": "成品項目", - "kinds": "款", - "Completed Date": "完成日期", - "Completed Time": "完成時間", - "Delivery Order": "送貨單", - "items": "項目", - "Select Pick Order:": "選擇提料單:", - "No Stock Available": "沒有庫存可用", - "is expired. Please check around have available QR code or not.": "已過期。請檢查周圍是否有可用的 QR 碼。", - "Start Fail": "開始失敗", - "Start PO": "開始採購訂單", - "Do you want to complete?": "確定完成嗎?", - "Complete": "完成", - "Complete Success": "完成成功", - "Complete Fail": "完成失敗", - "Complete Pick Order": "完成提料單", - "General": "一般", - "Bind Storage": "綁定倉位", - "itemNo": "貨品編號", - "itemName": "貨品名稱", - "qty": "訂單數", - "Require Qty": "需求數", - "uom": "計量單位", - "total weight": "總重量", - "weight unit": "重量單位", - "price": "訂單貨值", - "processed": "已入倉", - "expiryDate": "到期日", - "acceptedQty": "是次訂單/來貨/巳來貨數", - "weight": "重量", - "start": "開始", - "qc": "質量控制", - "escalation": "上報", - "stock in": "入庫", - "putaway": "上架", - "delete": "刪除", - "qty cannot be greater than remaining qty": "數量不能大於剩餘數", - "Record pol": "記錄採購訂單", - "Add some entries!": "請添加條目", - "draft": "草稿", - "pending": "待處理", - "determine1": "上報1", - "determine2": "上報2", - "determine3": "上報3", - "receiving": "收貨中", - "received": "已收貨", - "completed": "已完成", - "rejected": "已拒絕", - "success": "成功", - "acceptedQty must not greater than": "接受數量不得大於", - "minimal value is 1": "最小值為1", - "value must be a number": "值必須是數字", - "qc Check": "質量控制檢查", - "Please select QC": "請選擇質量控制", - "failQty": "失敗數", - "select qc": "選擇質量控制", - "enter a failQty": "請輸入失敗數", - "qty too big": "數量過大", - "sampleRate": "抽樣率", - "sampleWeight": "樣本重量", - "totalWeight": "總重量", - "Escalation": "上報", - "to be processed": "待處理", - "Stock In Detail": "入庫詳情", - "productLotNo": "產品批號", - "receiptDate": "收貨日期", - "acceptedWeight": "接受重量", - "productionDate": "生產日期", - "reportQty": "上報數", - "Default Warehouse": "預設倉庫", - "Select warehouse": "選擇倉庫", - "Putaway Detail": "上架詳情", - "LotNo": "批號", - "Po Code": "採購訂單編號", - "No Warehouse": "沒有倉庫", - "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", - "Reject": "拒絕", - "submit": "確認提交", - "print": "列印", - "bind": "綁定", - "Total must equal Required Qty. Missing: {diff}": "總數量必須等於所需數量。缺少:{{diff}}", - "Total must equal Required Qty. Exceeds by: {diff}": "總數量必須等於所需數量。超出:{{diff}}", - "Batch": "批量", - "Single": "單量", - "Release Type": "放單類型", - "isExtra order": "加單", - "Etra": "加單", - "Exit Etra view": "離開加單檢視", - "Etra Pick Order Detail": "加單", - "No inventory lot lines for inventoryLotId": "此批次尚未上架", - "No inventory lot for stockInLineId": "此批次尚未上架", - "Etra incomplete badge tooltip": "當日未完成加單票:{{count}} 張(待處理/已發佈,不含已結案)", - "Etra incomplete badge tooltip none": "目前無未完成加單票", - "Back to normal assign tab": "返回一般指派分頁", - "Enter isExtra workbench view?": "進入加單檢視?", - "Etra view groups all add-on tickets by shop and lane for the selected date.": "加單檢視會依選定日期,將 isExtra 票依店鋪與車線顯示。", - "Etra Ticket Notice": "目前是加單票,顯示與操作已切換為加單模式。", - "Merge Etra ticket": "合併加單送貨訂單", - "Merge Etra ticket dialog title": "合併加單送貨訂單和批次送貨訂單", - "Merge Etra ticket lane hint": "僅可合併同一店鋪、同一樓層(2/F、4/F 或車線-X)、同一車線與發車時間的未指派送貨訂單。左側可選批次/單張票或 TI-M 合併票。", - "Merge left column title": "批次/單張/合併票", - "Merge ticket TI-M": "TI-M 合併票", - "Batch/Single ticket": "批次/單張 送貨訂單", - "Etra ticket": "加單送貨訂單", - "No mergeable batch tickets": "暫無可合併的批次/單張 送貨訂單", - "No isExtra on same lane": "同車線暫無可合併的加單送貨訂單", - "Select batch ticket first for isExtra": "請先選擇左側批次/單張 送貨訂單", - "No delivery orders on ticket": "此票尚無送貨單", - "Merge Etra ticket confirm": "確認合併", - "Merge Etra ticket confirm title": "確認合併批次/加單送貨訂單?", - "Merge Etra ticket confirm hint": "將產生新 TI-M 合併票,原批次票與加單票將歸檔。", - "Merge Etra ticket confirm hint into TI-M": "加單票將併入所選 TI-M,原加單票將歸檔。", - "Merge Etra ticket success": "合併成功", - "Merge Etra ticket failed": "合併失敗", - "Next": "下一頁", - "Shop search": "店鋪搜尋", - "Confirm Search": "確認搜索", - "Merge Etra ticket search prompt": "請輸入店鋪(選購)與日期,點選「確認搜尋」載入可合併送貨訂單。", - "Merge Etra ticket search failed": "搜尋可合併送貨訂單失敗", - "Truck X": "車線-X", - "batch": "批量", - "single": "單張", - "isExtra": "加單", - "isExtrabatch": "合併批量", - "isExtrasingle": "合併單張", - "Pick Order": "提料單", - "Type": "類型", - "Product Type": "貨品類型", - "Reset": "重置", - "Search": "搜索", - "Pick Orders": "提料單", - "Consolidated Pick Orders": "合併提料單", - "Pick Order No.": "提料單編號", - "Pick Order Date": "提料單日期", - "Pick Order Status": "提貨狀態", - "Pick Order Type": "提料單類型", - "Consolidated Code": "合併編號", - "type": "類型", - "Items": "項目", - "Target Date": "需求日期", - "Released By": "發佈者", - "Target Date From": "目標日期", - "Target Date To": "目標日期到", - "Consolidate": "合併", - "Stock Unit": "庫存單位", - "create": "新增", - "detail": "詳情", - "Pick Order Detail": "撳單/提料單詳情", - "item": "貨品", - "Unit": "單位", - "reset": "重置", - "targetDate": "目標日期", - "remove": "移除", - "release": "發佈", - "location": "位置", - "suggestedLotNo": "建議批次", - "lotNo": "批次", - "item name": "貨品名稱", - "Item Name": "貨品名稱", - "approval": "審核", - "lot change": "批次變更", - "checkout": "出庫", - "Search Items": "搜索貨品", - "Search Results": "可選擇貨品", - "Second Search Results": "第二搜索結果", - "Second Search Items": "第二搜索項目", - "Second Search": "第二搜索", - "Item": "貨品", - "Order Quantity": "貨品需求數", - "Current Stock": "現時可用庫存", - "Selected": "已選", - "Select Items": "選擇貨品", - "Assign": "分派提料單", - "Release": "放單", - "Pick Execution": "進行提料", - "Create Pick Order": "建立貨品提料單", - "Consumable": "消耗品", - "Material": "食材", - "Job Order": "工單", - "End Product": "成品", - "Lot Expiry Date": "到期日", - "Lot Location": "位置", - "Available Lot": "可用提料數", - "Lot Required Pick Qty": "所需數", - "Lot Actual Pick Qty": "此單將提數", - "Lot#": "批號", - "Submit": "提交", - "Created Items": "已建立貨品", - "Create New Group": "建立新提料分組", - "Group": "分組", - "Qty Already Picked": "已提料數", - "Select Job Order Items": "選擇工單貨品", - "failedQty": "不合格項目數", - "remarks": "備註", - "Qc items": "QC 項目", - "qcItem": "QC 項目", - "QC Info": "QC 資訊", - "qcResult": "QC 結果", - "acceptQty": "接受數", - "Escalation History": "上報歷史", - "Group Code": "分組編號", - "Job Order Code": "工單編號", - "QC Check": "QC 檢查", - "QR Code Scan": "QR Code掃描", - "Pick Order Details": "提料單詳情", - "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", - "Pick order completed successfully!": "提料單完成成功!", - "Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。", - "This order is insufficient, please pick another lot.": "此訂單不足,請選擇其他批號。", - "Please finish QR code scan, QC check and pick order.": "請完成 QR 碼掃描、QC 檢查和提料。", - "No data available": "沒有資料", - "Please submit the pick order.": "請提交提料單。", - "Item lot to be Pick:": "批次貨品提料:", - "Report and Pick another lot": "上報並需重新選擇批號", - "Accept Stock Out": "接受出庫", - "Pick Another Lot": "欠數,並重新選擇批號", - "Delivery Note Code": "送貨單編號", - "A4 Printer": "A4 打印機", - "Label Printer": "標籤打印機", - "Please select a printer first": "請先選擇打印機", - "Please select a label printer first": "請先選擇標籤打印機", - "Lot No": "批號", - "Expiry Date": "到期日", - "Location": "位置", - "All Pick Order Lots": "所有提料單批次", - "Completed": "已完成", - "Finished Good Order": "成品出倉", - "Assign and Release": "分派並放單", - "Original Available Qty": "原可用數", - "Remaining Available Qty": "剩餘可用數", - "Please submit pick order.": "請提交提料單。", - "Please finish QR code scan and pick order.": "請完成 QR 碼掃描和提料。", - "Please finish QR code scanand pick order.": "請完成 QR 碼掃描和提料。", - "First created group": "首次建立分組", - "Latest created group": "最新建立分組", - "Manual Input": "手動輸入", - "QR Code Scan for Lot": " QR 碼掃描批次", - "Processing QR code...": "處理 QR 碼...", - "The input is not the same as the expected lot number.": "輸入的批次號碼與預期的不符。", - "Verified successfully!": "驗證成功!", - "Cancel": "取消", - "storing": "待品檢入倉", - "pick successful": "提料成功", - "Insufficient available quantity on lot (may have been picked by another user)": "掃描的批次已被其他用戶完全提料。請掃描其他批次。", - "Scan": "掃描", - "Before today": "今天之前", - "Scanned": "已掃描", - "Loading data...": "正在載入數據...", - "No available stock for this item": "沒有可用庫存", - "No lot details available for this item": "沒有批次詳情", - "Current stock is insufficient or unavailable": "現時可用庫存不足或不可用", - "Please check inventory status": "請檢查庫存狀態", - "Rows per page": "每頁行數", - "QR Scan Result:": "QR 掃描結果:", - "Action": "操作", - "Please finish pick order.": "請完成提料。", - "Lot": "批號", - "Assign Pick Orders": "分派提料單", - "Selected Pick Orders": "已選擇提料單數量", - "Please assgin/release the pickorders to picker": "請分派/放單提料單給提料員。", - "Assign To": "分派給", - "No Group": "沒有分組", - "Selected items will join above created group": "已選擇的貨品將加入以上建立的分組", - "Issue": "問題", - "Pick Execution Issue Form": "提料問題表單", - "Lot line is unavailable": "掃描批次不可用", - "This form is for reporting issues only. You must report either missing items or bad items.": "此表單僅用於報告問題。您必須報告缺少的物品或不良物品。", - "Bad item Qty": "不良貨品數量", - "Missing item Qty": "貨品遺失數量", - "Missing Item Qty": "貨品遺失數量", - "Bad Item Qty": "不良貨品數量", - "Bad Package Qty": "不良包裝數量", - "Lot line is not available (status=UNAVAILABLE)": "掃描批次不可用", - "Actual Pick Qty": "實際提料數量", - "Required Qty": "所需數量", - "Issue Remark": "問題描述", - "Handler": "提料員", - "Qty is required": "必需輸入數量", - "Qty is not allowed to be greater than remaining available qty": "輸入數量不能大於剩餘可用數量", - "Qty is not allowed to be greater than required qty": "輸入數量不能大於所需數量", - "At least one issue must be reported": "至少需要報告一個問題", - "issueRemark": "問題描述是必需的", - "handler": "提料員", - "Max": "最大值", - "Route": "路線", - "Index": "編號", - "No FG pick orders found": "沒有成品提料單", - "Finish Scan?": "完成掃描?", - "Delivery Code": "出倉單編號", - "Shop PO Code": "訂單編號", - "Shop ID": "商店編號", - "Truck No.": "車線編號", - "Departure Time": "車線出發時間", - "Shop Name": "商店名稱", - "Shop Address": "商店地址", - "Delivery Date": "目標日期", - "Pick Execution 2/F": "進行提料 2/F", - "Pick Execution 4/F": "進行提料 4/F", - "Pick Execution Detail": "進行提料詳情", - "Submit Required Pick Qty": "提交所需提料數量", - "Scan Result": "掃描結果", - "Ticket No.": "提票號碼", - "Start QR Scan": "開始QR掃描", - "Stop QR Scan": "停止QR掃描", - "Scanning...": "掃描中...", - "Print DN/Label": "列印送貨單/標籤", - "Store ID": "儲存編號", - "QR code does not match any item in current orders.": "QR 碼不符合當前訂單中的任何貨品。", - "Lot Number Mismatch": "批次號碼不符", - "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?": "掃描的貨品與預期的貨品相同,但批次號碼不同。您是否要繼續使用不同的批次?", - "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.": "掃描貨品相同但批次不同。請再掃描一次以確認:掃描「建議批次」的 QR 可沿用該批次;再掃描「另一批次」的 QR 則切換為該批次。", - "Expected Lot:": "預期批次:", - "Scanned Lot:": "掃描批次:", - "Confirm": "確認", - "Update your suggested lot to the this scanned lot": "更新您的建議批次為此掃描的批次", - "Print Draft": "列印草稿", - "Print Pick Order and DN Label": "列印提料單和送貨單標籤", - "Print Pick Order": "列印提料單", - "Print DN Label": "列印送貨單標籤", - "Print All Draft": "列印全部草稿", - "If you confirm, the system will:": "如果您確認,系統將:", - "After you scan to choose, the system will update the pick line to the lot you confirmed.": "確認後,系統會將您選擇的批次套用到對應提料行。", - "Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).": "若無法再掃描,可按下「確認」以切換為剛才掃描到的批次(與再掃一次該批次 QR 相同)。", - "Lot switch failed": "批次切換失敗", - "The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry.": "系統無法切換至掃描的批次。請核對下方批次後按「確認」重試。", - "You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch.": "您也可以再掃描:掃描建議批次 QR 可保留該行;掃描欲切換批次 QR 可再次嘗試切換。", - "QR code verified.": "QR 碼驗證成功。", - "Order Finished": "訂單完成", - "Submitted Status": "提交狀態", - "Pick Execution Record": "提料執行記錄", - "Delivery No.": "送貨單編號", - "Total": "總數", - "Completed DO pick orders: ": "已完成送貨單提料單:", - "No completed DO pick orders found": "沒有已完成送貨單提料單", - "Enter the number of cartons: ": "請輸入總箱數", - "Number of cartons": "箱數", - "Select an action for the assigned pick orders.": "選擇分配提料單的動作。", - "Detail": "詳情", - "consoCode": "合併編號", - "status": "狀態", - "Items Included": "貨品包括", - "Pick Order Included": "提料單包括", - "No created items": "沒有已建立的貨品", - "Please select item": "請選擇貨品", - "enter a qty": "請輸入數量", - "update qc info": "更新QC資訊", - "All lots must be completed before printing": "所有批次必須完成才能打印", - "Assigning pick order...": "分配提料單...", - "Enter the number of cartons:": "請輸入總箱數", - "Finished Good Detail": "成品提貨詳情", - "Finished Good Record": "成品提貨記錄", - "Finished Good Record (All)": "成品提貨記錄(全部)", - "All dates": "全部日期", - "Search date": "搜索日期", - "Hide Completed: OFF": "完成: OFF", - "Hide Completed: ON": "完成: ON", - "Number must be at least 1": "數量至少為1", - "Printed Successfully.": "成功列印", - "Product": "產品", - "You need to enter a number": "您需要輸入一個數字", - "Available in warehouse": "在倉庫中可用", - "Describe the issue with bad items": "描述不良貨品的問題", - "Enter pick qty or issue qty": "請輸入提料數量或不良數量", - "Invalid qty": "無效數量", - "Note:": "注意:", - "Qty is not allowed to be greater than required/available qty": "數量不能大於所需/可用數量", - "Still need to pick": "仍需提料", - "Total exceeds required qty": "總數超過所需數量", - "submitting": "提交中", - "Back to List": "返回列表", - "Delivery No": "送貨單編號", - "FG orders": "成品訂單", - "View Details": "查看詳情", - "No Item": "沒有貨品", - "None": "沒有", - "Add Selected Items to Created Items": "將已選擇的貨品添加到已建立的貨品中", - "All pick orders created successfully": "所有提料單建立成功", - "Failed to create group": "建立分組失敗", - "Invalid date format": "日期格式無效", - "Item already exists in created items": "貨品已存在於已建立的貨品中", - "Job Order not found or has no items": "工單不存在或沒有貨品", - "No results found": "沒有結果", - "Please enter at least code or name": "請輸入至少編號或名稱", - "Please enter quantity for all selected items": "請輸入所有已選擇的貨品的數量", - "Please select at least one item to submit": "請選擇至少一個貨品提交", - "Please select group and enter quantity for all selected items": "請選擇分組並輸入所有已選擇的貨品的數量", - "Please select group for all selected items": "請選擇分組對所有已選擇的貨品", - "Please select product type": "請選擇產品類型", - "Please select target date": "請選擇目標日期", - "Please select type": "請選擇類型", - "Search Criteria": "搜索條件", - "Processing...": "處理中", - "Failed items must have failed quantity": "不合格的貨品必須有不合格數量", - "QC items without result": "QC項目沒有結果", - "confirm putaway": "確認上架", - "email supplier": "發送郵件給供應商", - "qc processing": "QC處理", - "submitStockIn": "提交入庫", - "not default warehosue": "不是默認倉庫", - "printQty": "打印數量", - "Shop": "商店名稱", - "warehouse": "倉庫", - "Add Record": "添加記錄", - "Clean Record": "清空記錄", - "Select": "選擇", - "Close": "關閉", - "Truck": "車線", - "Date": "日期", - "Delivery Order Code": "送貨單編號", - "Escalation Info": "升級信息", - "Escalation Result": "升級結果", - "acceptQty must not greater than": "接受數量不能大於", - "supervisor": "主管", - "No Qc": "沒有QC", - "receivedQty": "接收數量", - "stock in information": "入庫信息", - "No Uom": "沒有單位", - "Input quantity cannot exceed": "輸入數量不能超過", - "Quantity cannot be negative": "數量不能為負數", - "Enter bad item quantity (required if no missing items)": "請輸入不良數量(如果沒有缺少項目)", - "Enter missing quantity (required if no bad items)": "請輸入缺少數量(如果沒有不良項目)", - "Submit All Scanned": "提交所有已掃描項目", - "Submitting...": "提交中...", - "COMPLETED": "已完成", - "Confirm print: (": "確認列印全部草稿?(總數量:", - "piece(s))": "份)", - "Printing...": "列印中", - "Please wait...": "請稍後", - "No available pick order(s) for this floor.": "此樓層沒有可用的提料單", - "You already have a pick order in progess. Please complete it first before taking next pick order.": "請先完成目前的提料單,再提取下一張", - "Error occurred during assignment.": "提料單分配錯誤", - "Info": "消息", - "Warning": "警告", - "Error": "錯誤", - "Batch Print": "批量列印", - "No entries available": "該樓層未有需處理訂單", - "Today": "是日", - "Tomorrow": "翌日", - "packaging": "提料中", - "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", - "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有 QR 碼,可能有剛剛入庫、轉移入庫或轉移出庫的 QR 碼。", - "Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})", - "Day After Tomorrow": "後日", - "Select Date": "請選擇日期", - "Suggest Lot No.": "推薦批號", - "Search by Shop": "搜索商店", - "Search by Truck": "搜索貨車", - "Print DN & Label": "列印提料單和送貨單標籤", - "Print Label": "列印送貨單標籤", - "Reprint Label(s)": "補印標籤", - "Reprint DN Label": "補印送貨單標籤", - "From carton": "起始箱號", - "To carton": "結束箱號", - "Total cartons on shipment": "總箱數", - "From carton must be at least 1": "起始箱號必須至少為 1", - "To carton must be greater than or equal to from carton": "結束箱號必須大於或等於起始箱號", - "Total cartons on shipment must be at least 1": "總箱數必須至少為 1", - "To carton cannot be greater than total cartons on shipment": "結束箱號不能大於總箱數", - "Not Yet Finished Released Do Pick Orders": "未完成提料單", - "Not yet finished released do pick orders": "未完成提料單", - "Released orders not yet completed - click lane to select and assign": "未完成提料單- 點擊貨車班次選擇並分配", - "Ticket Release Table": "查看提貨情況", - "Please take one pick order before printing the draft.": "請先從「撳單/提料單詳情」頁面下方選取提料單,再列印草稿。", - "No released pick order records found.": "目前沒有可用的提料單。", - "EDT - Lane Code (Unassigned/Total)": "預計出發時間 - 貨車班次(未撳數/總單數)", - "Floor ticket": "票別(樓層)", - "2F ticket": "2/F 票", - "4F ticket": "4/F 票", - "4F lane panel legend": "貨車班次 — 裝載序(未撳數/總單數)", - "Loading sequence n": "板{{n}}", - "lot QR code": "批號 QR 碼", - "label Printer": "標籤打印機", - "Loading Sequence": "裝載序", - "Ticket No": "提票號碼", - "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "掃描的庫存批行為「不可用」,無法換批或綁定;揀貨行未更新。", - "is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。", - "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。", - "Lot confirmation failed. Please try again.": "確認批號失敗,請重試。", - "Powder Mixture": "箱料粉", - "This lot is not yet putaway": "此批次尚未上架", - "Cannot resolve new inventory lot line": "無法解析新批號庫存行(請確認已上架且資料正確)。", - "Pick order line item is null": "提料單行未關聯物品。", - "New lot line item does not match pick order line item": "新批號行的物品與提料單行不一致。", - "Pick order line {{id}} not found": "找不到有關提料單行的資料。", - "SuggestedPickLot not found for pickOrderLineId {{polId}}": "找不到該提料單行的建議揀貨批", - "SuggestedPickLot qty is invalid: {{qty}}": "建議揀貨數量無效:{{qty}}。", - "Reject switch lot: available {{available}} less than required {{required}}": "此批次貨品已被其他送貨單留起,請掃描其他批次。", - "Reject switch lot: picked {{picked}} already greater or equal required {{required}}": "換批被拒:已揀數量({{picked}})已達或超過建議量({{required}}),無法再拆分換批。", - "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。", - "No lot rows. Select a line in the table above.": "尚無批號資料。請在上方表格勾選一行提料單明細。", - "No stock out line for this lot": "此批號尚無出庫行,無法提交。", - "No data available for this pick order.": "此提料單沒有可用資料。", - "Report missing or bad items": "報告缺失或不良物品", - "passed": "合格", - "failed": "不合格", - "confirm_accept_with_fail": "有不合格檢查項目,確認接受出庫?", - "No pending pick line left for this item. It may already be completed or fully processed.": "此貨品已無待處理的提貨行(可能已完成或已處理完畢)。" -} + { + "Purchase Order": "採購訂單", + "Code": "編號", + "Pick Order Code": "提料單編號", + "Item Code": "貨品編號", + "OrderDate": "下單日期", + "Details": "詳情", + "Supplier": "供應商", + "Status": "來貨狀態", + "N/A": "不適用", + "Release Pick Orders": "放單", + "released": "已放單", + "Loading...": "載入中...", + "Suggestion success": "建議成功", + "Scan pick success": "掃描提料成功", + "Remark": "備註", + "Available Qty": "可用數量", + "Picked Qty": "已提料數量", + "Escalated": "上報狀態", + "NotEscalated": "無上報", + "Assigned To": "已分配", + "Progress": "進度", + "Select Remark": "選擇備註", + "Just Complete": "已完成", + "Skip": "跳過", + "if need just edit number, please scan the lot again": "如果需要只修改數量,請重新掃描批次。", + "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}": "總數量(實際提料 + 遺失 + 不良)不能超過可用數量:{{available}}", + "Confirm Assignment": "確認分配", + "Required Date": "所需日期", + "Store": "位置", + "Available Orders": "可用訂單", + "This lot is rejected, please scan another lot.": "此批次已拒收,請掃描另一個批次。", + "Lane Code": "車線號碼", + "Fetching all matching records...": "正在獲取所有匹配的記錄...", + "Edit": "改數", + "Submit Qty": "提交數量", + "Suggestion success": "建議成功", + "Just Completed": "已完成", + "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.": "已完成(工作台):需有效批號與可提交數量;過期列請勿使用此按鈕。", + "Do you want to start?": "確定開始嗎?", + "Start": "開始", + "Pick Order Code(s)": "提料單編號", + "Delivery Order Code(s)": "提料單編號", + "Start Success": "開始成功", + "Qty will submit": "提交數量", + "Truck Lance Code": "車線號碼", + "Pick Order Codes": "提料單編號", + "Pick Order Lines": "提料單行數", + "Delivery Order Codes": "提料單編號", + "Delivery Order Lines": "送貨單行數", + "Lines Per Pick Order": "每提料單行數", + "Pick Orders Details": "提料單詳情", + "Lines": "行數", + "Before Today": "以前", + "Truck X": "車線-X", + "Finsihed good items": "成品項目", + "kinds": "款", + "Completed Date": "完成日期", + "Completed Time": "完成時間", + "Delivery Order": "送貨單", + "items": "項目", + "Select Pick Order:": "選擇提料單:", + "No Stock Available": "沒有庫存", + "is expired. Please check around have available QR code or not.": "已過期。請檢查周圍是否有可用的 QR 碼。", + "Start Fail": "開始失敗", + "Start PO": "開始採購訂單", + "Do you want to complete?": "確定完成嗎?", + "Complete": "完成", + "Complete Success": "完成成功", + "Complete Fail": "完成失敗", + "Complete Pick Order": "完成提料單", + "General": "一般", + "Bind Storage": "綁定倉位", + "itemNo": "貨品編號", + "itemName": "貨品名稱", + "qty": "訂單數", + "Require Qty": "需求數", + "uom": "計量單位", + "total weight": "總重量", + "weight unit": "重量單位", + "price": "訂單貨值", + "processed": "已入倉", + "expiryDate": "到期日", + "acceptedQty": "是次訂單/來貨/巳來貨數", + "weight": "重量", + "start": "開始", + "qc": "質量控制", + "escalation": "上報", + "stock in": "入庫", + "putaway": "上架", + "delete": "刪除", + "qty cannot be greater than remaining qty": "數量不能大於剩餘數", + "Record pol": "記錄採購訂單", + "Add some entries!": "請添加條目", + "draft": "草稿", + "pending": "待處理", + "determine1": "上報1", + "determine2": "上報2", + "determine3": "上報3", + "receiving": "收貨中", + "received": "已收貨", + "completed": "已完成", + "rejected": "已拒絕", + "success": "成功", + "acceptedQty must not greater than": "接受數量不得大於", + "minimal value is 1": "最小值為1", + "value must be a number": "值必須是數字", + "qc Check": "質量控制檢查", + "Please select QC": "請選擇質量控制", + "failQty": "失敗數", + "select qc": "選擇質量控制", + "enter a failQty": "請輸入失敗數", + "qty too big": "數量過大", + "sampleRate": "抽樣率", + "sampleWeight": "樣本重量", + "totalWeight": "總重量", + + "Escalation": "上報", + "to be processed": "待處理", + + "Stock In Detail": "入庫詳情", + "productLotNo": "產品批號", + "receiptDate": "收貨日期", + "acceptedWeight": "接受重量", + "productionDate": "生產日期", + + "reportQty": "上報數", + + "Default Warehouse": "預設倉庫", + "Select warehouse": "選擇倉庫", + "Putaway Detail": "上架詳情", + "LotNo": "批號", + "Po Code": "採購訂單編號", + "No Warehouse": "沒有倉庫", + "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", + + "Reject": "拒絕", + "submit": "確認提交", + "print": "列印", + "bind": "綁定", + "Total must equal Required Qty. Missing: {diff}": "總數量必須等於所需數量。缺少:{{diff}}", + "Total must equal Required Qty. Exceeds by: {diff}": "總數量必須等於所需數量。超出:{{diff}}", + + "Batch": "批量", + "Single": "單量", + "Release Type": "放單類型", + "isExtra order": "加單", + "Etra": "加單", + "Exit Etra view": "離開加單檢視", + "Etra Pick Order Detail": "加單", + "Etra incomplete badge tooltip": "當日未完成加單票:{{count}} 張(待處理/已發佈,不含已結案)", + "Etra incomplete badge tooltip none": "目前無未完成加單票", + "Back to normal assign tab": "返回一般指派分頁", + "Enter isExtra workbench view?": "進入加單檢視?", + "Etra view groups all add-on tickets by shop and lane for the selected date.": "加單檢視會依選定日期,將 isExtra 票依店鋪與車線顯示。", + "Etra Ticket Notice": "目前是加單票,顯示與操作已切換為加單模式。", + + "Pick Order": "提料單", + "Type": "類型", + "Product Type": "貨品類型", + "Reset": "重置", + "Search": "搜索", + "Pick Orders": "提料單", + "Consolidated Pick Orders": "合併提料單", + "Pick Order No.": "提料單編號", + "Pick Order Date": "提料單日期", + "Pick Order Status": "提貨狀態", + "Pick Order Type": "提料單類型", + "Consolidated Code": "合併編號", + "type": "類型", + "Items": "項目", + "Target Date": "需求日期", + "Released By": "發佈者", + "Target Date From": "目標日期", + "Target Date To": "目標日期到", + "Consolidate": "合併", + "Stock Unit": "庫存單位", + "create": "新增", + "detail": "詳情", + "Pick Order Detail": "撳單/提料單詳情", + "item": "貨品", + "Unit": "單位", + "reset": "重置", + "targetDate": "目標日期", + "remove": "移除", + "release": "發佈", + "location": "位置", + "suggestedLotNo": "建議批次", + "lotNo": "批次", + "item name": "貨品名稱", + "Item Name": "貨品名稱", + "approval": "審核", + "lot change": "批次變更", + "checkout": "出庫", + "Search Items": "搜索貨品", + "Search Results": "可選擇貨品", + "Second Search Results": "第二搜索結果", + "Second Search Items": "第二搜索項目", + "Second Search": "第二搜索", + "Item": "貨品", + "Order Quantity": "貨品需求數", + "Current Stock": "現時可用庫存", + "Selected": "已選", + "Select Items": "選擇貨品", + "Assign": "分派提料單", + "Release": "放單", + "Pick Execution": "進行提料", + "Create Pick Order": "建立貨品提料單", + "Consumable": "消耗品", + "Material": "食材", + "Job Order": "工單", + "End Product": "成品", + "Lot Expiry Date": "到期日", + "Lot Location": "位置", + "Available Lot": "可用提料數", + "Lot Required Pick Qty": "所需數", + "Lot Actual Pick Qty": "此單將提數", + "Lot#": "批號", + "Submit": "提交", + "Created Items": "已建立貨品", + "Create New Group": "建立新提料分組", + "Group": "分組", + "Qty Already Picked": "已提料數", + "Select Job Order Items": "選擇工單貨品", + "failedQty": "不合格項目數", + "remarks": "備註", + "Qc items": "QC 項目", + "qcItem": "QC 項目", + "QC Info": "QC 資訊", + "qcResult": "QC 結果", + "acceptQty": "接受數", + "Escalation History": "上報歷史", + "Group Code": "分組編號", + "Job Order Code": "工單編號", + "QC Check": "QC 檢查", + "QR Code Scan": "QR Code掃描", + "Pick Order Details": "提料單詳情", + "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", + "Pick order completed successfully!": "提料單完成成功!", + "Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。", + "This order is insufficient, please pick another lot.": "此訂單不足,請選擇其他批號。", + "Please finish QR code scan, QC check and pick order.": "請完成 QR 碼掃描、QC 檢查和提料。", + "No data available": "沒有資料", + "Please submit the pick order.": "請提交提料單。", + "Item lot to be Pick:": "批次貨品提料:", + "Report and Pick another lot": "上報並需重新選擇批號", + "Accept Stock Out": "接受出庫", + "Pick Another Lot": "欠數,並重新選擇批號", + "Delivery Note Code": "送貨單編號", + "A4 Printer": "A4 打印機", + "Label Printer": "標籤打印機", + "Please select a printer first": "請先選擇打印機", + "Please select a label printer first": "請先選擇標籤打印機", + + "Lot No": "批號", + "Expiry Date": "到期日", + "Location": "位置", + "All Pick Order Lots": "所有提料單批次", + "Completed": "已完成", + "Finished Good Order": "成品出倉", + "Assign and Release": "分派並放單", + "Original Available Qty": "原可用數", + "Remaining Available Qty": "剩餘可用數", + "Please submit pick order.": "請提交提料單。", + "Please finish QR code scan and pick order.": "請完成 QR 碼掃描和提料。", + "Please finish QR code scanand pick order.": "請完成 QR 碼掃描和提料。", + "First created group": "首次建立分組", + "Latest created group": "最新建立分組", + "Manual Input": "手動輸入", + "QR Code Scan for Lot": " QR 碼掃描批次", + "Processing QR code...": "處理 QR 碼...", + "The input is not the same as the expected lot number.": "輸入的批次號碼與預期的不符。", + "Verified successfully!": "驗證成功!", + "Cancel": "取消", + "storing": "待品檢入倉", + "pick successful": "提料成功", + "Suggestion success": "建議成功", + "Insufficient available quantity on lot (may have been picked by another user)": "掃描的批次已被其他用戶完全提料。請掃描其他批次。", + "Scan": "掃描", + "Before today": "今天之前", + "Scanned": "已掃描", + "Loading data...": "正在載入數據...", + "No available stock for this item": "沒有可用庫存", + "No lot details available for this item": "沒有批次詳情", + "Current stock is insufficient or unavailable": "現時可用庫存不足或不可用", + "Please check inventory status": "請檢查庫存狀態", + "Rows per page": "每頁行數", + "QR Scan Result:": "QR 掃描結果:", + "Action": "操作", + "Please finish pick order.": "請完成提料。", + "Lot": "批號", + "Assign Pick Orders": "分派提料單", + "Selected Pick Orders": "已選擇提料單數量", + "Please assgin/release the pickorders to picker": "請分派/放單提料單給提料員。", + "Assign To": "分派給", + "No Group": "沒有分組", + "Selected items will join above created group": "已選擇的貨品將加入以上建立的分組", + "Issue":"問題", + "Pick Execution Issue Form":"提料問題表單", + "Lot line is unavailable":"掃描批次不可用", + "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", + "Bad item Qty":"不良貨品數量", + "Missing item Qty":"貨品遺失數量", + "Missing Item Qty":"貨品遺失數量", + "Bad Item Qty":"不良貨品數量", + "Bad Package Qty":"不良包裝數量", + "Lot line is not available (status=UNAVAILABLE)":"掃描批次不可用", + "Actual Pick Qty":"實際提料數量", + "Required Qty":"所需數量", + "Issue Remark":"問題描述", + "Handler":"提料員", + "Qty is required":"必需輸入數量", + "Qty is not allowed to be greater than remaining available qty":"輸入數量不能大於剩餘可用數量", + "Qty is not allowed to be greater than required qty":"輸入數量不能大於所需數量", + "At least one issue must be reported":"至少需要報告一個問題", + "issueRemark":"問題描述是必需的", + "handler":"提料員", + "Max":"最大值", + "Route":"路線", + "Index":"編號", + "No FG pick orders found":"沒有成品提料單", + "Finish Scan?":"完成掃描?", + "Delivery Code":"出倉單編號", + "Shop PO Code":"訂單編號", + "Shop ID":"商店編號", + "Truck No.":"車線編號", + "Departure Time":"車線出發時間", + "Shop Name":"商店名稱", + "Shop Address":"商店地址", + "Delivery Date":"目標日期", + "Pick Execution 2/F":"進行提料 2/F", + "Pick Execution 4/F":"進行提料 4/F", + "Pick Execution Detail":"進行提料詳情", + "Submit Required Pick Qty":"提交所需提料數量", + "Scan Result":"掃描結果", + "Ticket No.":"提票號碼", + "Start QR Scan":"開始QR掃描", + "Stop QR Scan":"停止QR掃描", + "Scanning...":"掃描中...", + "Print DN/Label":"列印送貨單/標籤", + "Store ID":"儲存編號", + "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。", + "Lot Number Mismatch":"批次號碼不符", + "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?":"掃描的貨品與預期的貨品相同,但批次號碼不同。您是否要繼續使用不同的批次?", + "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.":"掃描貨品相同但批次不同。請再掃描一次以確認:掃描「建議批次」的 QR 可沿用該批次;再掃描「另一批次」的 QR 則切換為該批次。", + "Expected Lot:":"預期批次:", + "Scanned Lot:":"掃描批次:", + "Confirm":"確認", + "Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次", + "Print Draft":"列印草稿", + "Print Pick Order and DN Label":"列印提料單和送貨單標籤", + "Print Pick Order":"列印提料單", + "Print DN Label":"列印送貨單標籤", + "Print All Draft" : "列印全部草稿", + "If you confirm, the system will:":"如果您確認,系統將:", + "After you scan to choose, the system will update the pick line to the lot you confirmed.":"確認後,系統會將您選擇的批次套用到對應提料行。", + "Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).":"若無法再掃描,可按下「確認」以切換為剛才掃描到的批次(與再掃一次該批次 QR 相同)。", + "Lot switch failed":"批次切換失敗", + "The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry.":"系統無法切換至掃描的批次。請核對下方批次後按「確認」重試。", + "You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch.":"您也可以再掃描:掃描建議批次 QR 可保留該行;掃描欲切換批次 QR 可再次嘗試切換。", + "QR code verified.":"QR 碼驗證成功。", + "Order Finished":"訂單完成", + "Submitted Status":"提交狀態", + "Pick Execution Record":"提料執行記錄", + "Delivery No.":"送貨單編號", + "Total":"總數", + "Completed DO pick orders: ":"已完成送貨單提料單:", + "No completed DO pick orders found":"沒有已完成送貨單提料單", + + "Enter the number of cartons: ": "請輸入總箱數", + "Number of cartons": "箱數", + "Select an action for the assigned pick orders.": "選擇分配提料單的動作。", + "Detail": "詳情", + "consoCode": "合併編號", + "status": "狀態", + "Items Included": "貨品包括", + "Pick Order Included": "提料單包括", + "No created items": "沒有已建立的貨品", + "Please select item": "請選擇貨品", + "enter a qty": "請輸入數量", + "update qc info": "更新QC資訊", + "All lots must be completed before printing": "所有批次必須完成才能打印", + "Assigning pick order...": "分配提料單...", + "Enter the number of cartons:": "請輸入總箱數", + "Finished Good Detail": "成品提貨詳情", + "Finished Good Record": "成品提貨記錄", + "Finished Good Record (All)": "成品提貨記錄(全部)", + "All dates": "全部日期", + "Search date": "搜索日期", + "Hide Completed: OFF": "完成: OFF", + "Hide Completed: ON": "完成: ON", + "Number must be at least 1": "數量至少為1", + "Printed Successfully.": "成功列印", + "Product": "產品", + "You need to enter a number": "您需要輸入一個數字", + "Available in warehouse": "在倉庫中可用", + "Describe the issue with bad items": "描述不良貨品的問題", + "Enter pick qty or issue qty": "請輸入提料數量或不良數量", + "Invalid qty": "無效數量", + "Note:": "注意:", + "Qty is not allowed to be greater than required/available qty": "數量不能大於所需/可用數量", + "Still need to pick": "仍需提料", + "Total exceeds required qty": "總數超過所需數量", + "submitting": "提交中", + "Back to List": "返回列表", + "Delivery No": "送貨單編號", + "FG orders": "成品訂單", + "View Details": "查看詳情", + "No Item": "沒有貨品", + "None": "沒有", + "This form is for reporting issues only. You must report either missing items or bad items.": "此表單僅用於報告問題。您必須報告缺少的物品或不良物品。", + "Add Selected Items to Created Items": "將已選擇的貨品添加到已建立的貨品中", + "All pick orders created successfully": "所有提料單建立成功", + "Failed to create group": "建立分組失敗", + "Invalid date format": "日期格式無效", + "Item already exists in created items": "貨品已存在於已建立的貨品中", + "Job Order not found or has no items": "工單不存在或沒有貨品", + "Loading...": "加載中", + "No results found": "沒有結果", + "Please enter at least code or name": "請輸入至少編號或名稱", + "Please enter quantity for all selected items": "請輸入所有已選擇的貨品的數量", + "Please select at least one item to submit": "請選擇至少一個貨品提交", + "Please select group and enter quantity for all selected items": "請選擇分組並輸入所有已選擇的貨品的數量", + "Please select group for all selected items": "請選擇分組對所有已選擇的貨品", + "Please select product type": "請選擇產品類型", + "Please select target date": "請選擇目標日期", + "Please select type": "請選擇類型", + "Search Criteria": "搜索條件", + "Processing...": "處理中", + "Failed items must have failed quantity": "不合格的貨品必須有不合格數量", + "QC items without result": "QC項目沒有結果", + "confirm putaway": "確認上架", + "email supplier": "發送郵件給供應商", + "qc processing": "QC處理", + "submitStockIn": "提交入庫", + "not default warehosue": "不是默認倉庫", + "printQty": "打印數量", + "Shop": "商店名稱", + "warehouse": "倉庫", + "Add Record": "添加記錄", + "Clean Record": "清空記錄", + "Select": "選擇", + "Close": "關閉", + "Truck": "車線", + "Date": "日期", + "Delivery Order Code": "送貨單編號", + "Escalation Info": "升級信息", + "Escalation Result": "升級結果", + "acceptQty must not greater than": "接受數量不能大於", + "supervisor": "主管", + "No Qc": "沒有QC", + "receivedQty": "接收數量", + "stock in information": "入庫信息", + "No Uom": "沒有單位", + "Input quantity cannot exceed": "輸入數量不能超過", + "Quantity cannot be negative": "數量不能為負數", + "Enter bad item quantity (required if no missing items)": "請輸入不良數量(如果沒有缺少項目)", + "Enter missing quantity (required if no bad items)": "請輸入缺少數量(如果沒有不良項目)", + "Submit All Scanned": "提交所有已掃描項目", + "Submitting...": "提交中...", + "COMPLETED": "已完成", + "Confirm print: (": "確認列印全部草稿?(總數量:", + "piece(s))": "份)", + "Printing...": "列印中", + "Please wait...": "請稍後", + "No available pick order(s) for this floor.": "此樓層沒有可用的提料單", + "You already have a pick order in progess. Please complete it first before taking next pick order.": "請先完成目前的提料單,再提取下一張", + "Error occurred during assignment.": "提料單分配錯誤", + "Info": "消息", + "Warning": "警告", + "Error": "錯誤", + "Batch Print": "批量列印", + "No entries available": "該樓層未有需處理訂單", + "Today": "是日", + "Tomorrow": "翌日", + "packaging": "提料中", + "No Stock Available": "沒有庫存可用", + "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", + "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有 QR 碼,可能有剛剛入庫、轉移入庫或轉移出庫的 QR 碼。", + "Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})", + "Day After Tomorrow": "後日", + "Lot line is unavailable": "掃描批次不可用", + "Select Date": "請選擇日期", + "Suggest Lot No.": "推薦批號", + "Search by Shop": "搜索商店", + "Search by Truck": "搜索貨車", + "Print DN & Label": "列印提料單和送貨單標籤", + "Print Label": "列印送貨單標籤", + "Print Blank Page Labels": "列印空白頁數標籤", + "Enter blank label print quantity:": "請輸入要列印的標籤數量:", + "Label print quantity": "標籤數量", + "Reprint Label(s)": "補印標籤", + "Reprint DN Label": "補印送貨單標籤", + "From carton": "起始箱號", + "To carton": "結束箱號", + "Total cartons on shipment": "總箱數", + "From carton must be at least 1": "起始箱號必須至少為 1", + "To carton must be greater than or equal to from carton": "結束箱號必須大於或等於起始箱號", + "Total cartons on shipment must be at least 1": "總箱數必須至少為 1", + "To carton cannot be greater than total cartons on shipment": "結束箱號不能大於總箱數", + "Not Yet Finished Released Do Pick Orders": "未完成提料單", + "Not yet finished released do pick orders": "未完成提料單", + "Released orders not yet completed - click lane to select and assign": "未完成提料單- 點擊貨車班次選擇並分配", + "Ticket Release Table": "查看提貨情況", + "Please take one pick order before printing the draft.": "請先從「撳單/提料單詳情」頁面下方選取提料單,再列印草稿。", + "No released pick order records found.": "目前沒有可用的提料單。", + "EDT - Lane Code (Unassigned/Total)": "預計出發時間 - 貨車班次(未撳數/總單數)", + "Floor ticket": "票別(樓層)", + "2F ticket": "2/F 票", + "4F ticket": "4/F 票", + "4F lane panel legend": "貨車班次 — 裝載序(未撳數/總單數)", + "Loading sequence n": "板{{n}}", + "lot QR code": "批號 QR 碼", + "label Printer" : "標籤打印機", + "A4 Printer" : "A4 打印機", + "Loading Sequence": "裝載序", + "Ticket No": "提票號碼", + "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "掃描的庫存批行為「不可用」,無法換批或綁定;揀貨行未更新。", + "is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。", + "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。", + "Lot confirmation failed. Please try again.": "確認批號失敗,請重試。", + "Powder Mixture": "箱料粉", + "This lot is not yet putaway": "此批次尚未上架", + "Cannot resolve new inventory lot line": "無法解析新批號庫存行(請確認已上架且資料正確)。", + "Pick order line item is null": "提料單行未關聯物料。", + "New lot line item does not match pick order line item": "新批號行的物料與提料單行不一致。", + "Pick order line {{id}} not found": "找不到有關提料單行的資料。", + "SuggestedPickLot not found for pickOrderLineId {{polId}}": "找不到該提料單行的建議揀貨批", + "SuggestedPickLot qty is invalid: {{qty}}": "建議揀貨數量無效:{{qty}}。", + "Reject switch lot: available {{available}} less than required {{required}}": "此批次貨品已被其他送貨單留起,請掃描其他批次。", + "Reject switch lot: picked {{picked}} already greater or equal required {{required}}": "換批被拒:已揀數量({{picked}})已達或超過建議量({{required}}),無法再拆分換批。", + "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。", + "No lot rows. Select a line in the table above.": "尚無批號資料。請在上方表格勾選一行提料單明細。", + "No stock out line for this lot": "此批號尚無出庫行,無法提交。" + } \ No newline at end of file