From 163b634c7e592e92903f4d20f5f9379034e7a415 Mon Sep 17 00:00:00 2001 From: Harry Groves Date: Mon, 21 Sep 2026 12:41:41 +0800 Subject: [PATCH] [Fix] DO Workbench EN language --- .../DoWorkbench/DoWorkbenchTabs.tsx | 34 ++- .../TruckRoutingSummaryTabWorkbench.tsx | 25 +- .../WorkbenchGoodPickExecutionDetail.tsx | 42 ++-- .../WorkbenchLotLabelPrintModal.tsx | 92 ++++---- .../FinishedGoodCartonDashboardTab.tsx | 101 ++++---- .../WorkbenchPickExecution.tsx | 8 +- src/i18n/en/common.json | 220 +++++++++--------- src/i18n/en/doWorkbench.json | 21 +- src/i18n/en/pickOrder.json | 92 +++++++- src/i18n/en/ticketReleaseTable.json | 6 +- src/i18n/zh/doWorkbench.json | 16 +- src/i18n/zh/pickOrder.json | 81 ++++++- src/utils/workbenchPickLotUtils.ts | 38 ++- 13 files changed, 518 insertions(+), 258 deletions(-) diff --git a/src/components/DoWorkbench/DoWorkbenchTabs.tsx b/src/components/DoWorkbench/DoWorkbenchTabs.tsx index 69015ee6..c247d87b 100644 --- a/src/components/DoWorkbench/DoWorkbenchTabs.tsx +++ b/src/components/DoWorkbench/DoWorkbenchTabs.tsx @@ -186,7 +186,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom const confirmResult = await Swal.fire({ title: t("Batch Print"), - text: `${t("Confirm print: (")}${releasedOrders.length}${t("piece(s))")}`, + text: t("Confirm print drafts", { count: releasedOrders.length }), icon: "question", showCancelButton: true, confirmButtonText: t("Confirm"), @@ -276,18 +276,29 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom @@ -297,7 +308,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom sx={{ overflow: "visible", /* 徽章在標籤右側外凸,預留空間避免與下一個 Tab 貼死 */ - pr: etraIncompleteDopoCount > 99 ? 5 : etraIncompleteDopoCount > 0 ? 4 : 2, + pr: etraIncompleteDopoCount > 99 ? 2.5 : etraIncompleteDopoCount > 0 ? 2 : 1, }} label={ = ({ defaultTabIndex = 0, printerCom 0 ? 1 : 0 }} + sx={{ + pr: etraIncompleteDopoCount > 0 ? 1 : 0, + whiteSpace: "normal", + lineHeight: 1.2, + textAlign: "center", + }} > {t("Etra Pick Order Detail")} @@ -341,8 +357,8 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom - - + + diff --git a/src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx b/src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx index 3325a539..c47689f1 100644 --- a/src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx +++ b/src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material"; import DownloadIcon from "@mui/icons-material/Download"; +import { useTranslation } from "react-i18next"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { @@ -18,6 +19,7 @@ import { } from "@/lib/featureUsageLog"; const TruckRoutingSummaryTabWorkbench: React.FC = () => { + const { t } = useTranslation(); const [storeOptions, setStoreOptions] = useState([]); const [laneOptions, setLaneOptions] = useState([]); const [storeId, setStoreId] = useState(""); @@ -45,6 +47,9 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { const canDownload = storeId && truckLanceCode && date && !loading; + const displayOptionLabel = (label: string) => + String(label).trim() === "車線-X" ? t("Truck X") : label; + const onDownload = async () => { if (!canDownload) return; try { @@ -55,7 +60,9 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { }); if (precheck.hasUnpickedOrders) { const confirmed = window.confirm( - `此車線仍有 ${precheck.unpickedOrderCount} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?` + t("Unpicked orders confirm download", { + count: precheck.unpickedOrderCount, + }), ); if (!confirmed) return; } @@ -92,7 +99,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { ); } catch (error) { console.error("Failed to download Workbench Truck Routing Summary", error); - alert("下載 Workbench 送貨路線摘要失敗,請稍後再試。"); + alert(t("Failed to download Workbench truck routing summary. Please try again later.")); } finally { setLoading(false); } @@ -101,39 +108,39 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { return ( - 送貨路線摘要 (Workbench) + {t("Truck Routing Summary (Workbench)")} onStoreChange(e.target.value)} > {storeOptions.map((opt) => ( - {opt.label} + {displayOptionLabel(opt.label)} ))} setTruckLanceCode(e.target.value)} disabled={!storeId} > {laneOptions.map((opt) => ( - {opt.label} + {displayOptionLabel(opt.label)} ))} { disabled={!canDownload} onClick={onDownload} > - {loading ? "生成中..." : "下載報告 (PDF)"} + {loading ? t("Generating...") : t("Download report (PDF)")} ); diff --git a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx index 29536591..91578a07 100644 --- a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx +++ b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx @@ -421,22 +421,30 @@ function isWorkbenchSourceLotExpired(lot: any): boolean { return false; } -function getWorkbenchSourceLotStatusSummary(lot: any): { +type PickOrderT = (key: string, options?: Record) => string; + +function getWorkbenchSourceLotStatusSummary(lot: any, t: PickOrderT): { severity: "success" | "warning" | "error"; text: string; } { if (!lot) { - return { severity: "warning", text: "無法判斷此批號狀態" }; + return { severity: "warning", text: t("Cannot determine this lot status") }; } if (isWorkbenchSourceLotExpired(lot)) { - return { severity: "error", text: "此批號狀態:已過期" }; + return { severity: "error", text: t("Lot status: expired") }; } const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); if (solSt === "rejected") { - return { severity: "warning", text: "此出庫行:已拒絕,請改掃其他批號" }; + return { + severity: "warning", + text: t("This pick line was rejected. Please scan another lot."), + }; } if (solSt === "completed" || solSt === "partially_completed") { - return { severity: "warning", text: "此出庫行:已完成,無需再提貨" }; + return { + severity: "warning", + text: t("This pick line is already completed. No further pick needed."), + }; } /** * 無批次列:後端仍標 insufficient_stock,語意是「尚無可出庫批號」而非「已用畢」。 @@ -449,28 +457,28 @@ function getWorkbenchSourceLotStatusSummary(lot: any): { if (isNoLotRow) { return { severity: "warning", - text: "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR", + text: t( + "No lot bound / no available stock line. Please scan nearby inbound or transfer QR.", + ), }; } const av = String(lot.lotAvailability || "").toLowerCase(); if (av === "insufficient_stock") { - return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" }; + return { severity: "warning", text: t("Lot status: depleted (no remaining stock)") }; } const avail = Number(lot.availableQty); if (lot.lotNo && Number.isFinite(avail) && avail <= 0) { - return { severity: "warning", text: "此批號狀態:已用畢(可用量為 0)" }; + return { severity: "warning", text: t("Lot status: depleted (available qty is 0)") }; } if (isInventoryLotLineUnavailable(lot)) { return { severity: "warning", - text: "此批號狀態:庫存不可用(未上架或行狀態不可用)", + text: t("Lot status: unavailable (not put away or line unavailable)"), }; } - return { severity: "success", text: "此批號狀態:可提貨" }; + return { severity: "success", text: t("Lot status: ready to pick") }; } -type PickOrderT = (key: string, options?: Record) => string; - function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string { const msg = raw.trim(); if (!msg) return msg; @@ -1334,9 +1342,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO severity: undefined as "success" | "warning" | "error" | undefined, }; } - const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot); + const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot, t); return { text: s.text, severity: s.severity }; - }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot]); + }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, t]); const workbenchLotLabelSubmitQty = useMemo(() => { if (!workbenchLotLabelContextLot) return 0; @@ -1811,7 +1819,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( - `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` + t("This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.", { + lot: scannedLot.lotNo || scannedStockInLineId, + }), ); }); // Mark this SOL as processed to prevent re-processing @@ -1864,7 +1874,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO startTransition(() => { setQrScanError(true); setQrScanSuccess(false); - setQrScanErrorMsg("当前订单中没有此物品的批次信息"); + setQrScanErrorMsg(t("No lot information for this item in the current order")); }); return; } diff --git a/src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx b/src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx index 2863a5b9..1d477005 100644 --- a/src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx +++ b/src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx @@ -37,6 +37,7 @@ import { printWorkbenchLotLabel, } from "@/app/api/doworkbench/actions"; import { QRCodeSVG } from "qrcode.react"; +import { useTranslation } from "react-i18next"; type ScanPayload = { itemId: number; @@ -167,6 +168,7 @@ const WorkbenchLotLabelPrintModal: React.FC = submitQty = null, onSubmitQtyChange, }) => { + const { t } = useTranslation(); const scanInputRef = useRef(null); const [scanInput, setScanInput] = useState(""); const [scanError, setScanError] = useState(null); @@ -210,8 +212,8 @@ const WorkbenchLotLabelPrintModal: React.FC = useEffect(() => { if (!open) return; resetAll(); - const t = setTimeout(() => scanInputRef.current?.focus(), 50); - return () => clearTimeout(t); + const focusTimer = setTimeout(() => scanInputRef.current?.focus(), 50); + return () => clearTimeout(focusTimer); }, [open, resetAll]); const loadPrinters = useCallback(async () => { @@ -224,13 +226,13 @@ const WorkbenchLotLabelPrintModal: React.FC = setPrinters([]); setSnackbar({ open: true, - message: e instanceof Error ? e.message : "載入印表機清單失敗", + message: e instanceof Error ? e.message : t("Failed to load printer list"), severity: "error", }); } finally { setPrintersLoading(false); } - }, []); + }, [t]); useEffect(() => { if (!open) return; @@ -283,23 +285,23 @@ const WorkbenchLotLabelPrintModal: React.FC = setAnalysis(data); setSnackbar({ open: true, - message: "已載入同品可用批號清單", + message: t("Loaded available lots for this item"), severity: "success", }); } catch (e) { setAnalysis(null); - setScanError(e instanceof Error ? e.message : "分析失敗"); + setScanError(e instanceof Error ? e.message : t("Analysis failed")); } finally { setAnalysisLoading(false); } }, - [resolveExpectedUomId], + [resolveExpectedUomId, t], ); const analyzeByItem = useCallback( async (itemId: number) => { if (!Number.isFinite(itemId) || itemId <= 0) { - setScanError("無效 itemId,無法載入批號清單。"); + setScanError(t("Invalid itemId, cannot load lot list.")); return; } setLastItemId(itemId); @@ -325,17 +327,17 @@ const WorkbenchLotLabelPrintModal: React.FC = }); setSnackbar({ open: true, - message: "已載入同品可用批號清單", + message: t("Loaded available lots for this item"), severity: "success", }); } catch (e) { setAnalysis(null); - setScanError(e instanceof Error ? e.message : "分析失敗"); + setScanError(e instanceof Error ? e.message : t("Analysis failed")); } finally { setAnalysisLoading(false); } }, - [resolveExpectedUomId], + [resolveExpectedUomId, t], ); const handleAnalyze = useCallback(async () => { @@ -343,13 +345,13 @@ const WorkbenchLotLabelPrintModal: React.FC = const payload = safeParseScanPayload(raw); if (!payload) { setScanError( - '掃碼內容格式錯誤,請重新掃碼', + t("Invalid scan format. Please scan again."), ); setAnalysis(null); return; } await analyzePayload(payload); - }, [scanInput, analyzePayload]); + }, [scanInput, analyzePayload, t]); const handleRefreshLots = useCallback(async () => { const payload = lastPayload ?? safeParseScanPayload(scanInput.trim()); @@ -368,12 +370,12 @@ const WorkbenchLotLabelPrintModal: React.FC = if (!payload) { setSnackbar({ open: true, - message: "請先掃碼或查詢一次,才可刷新批號清單。", + message: t("Scan or look up once before refreshing the lot list."), severity: "info", }); return; } - }, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput]); + }, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput, t]); useEffect(() => { if (!open) return; @@ -470,7 +472,7 @@ const WorkbenchLotLabelPrintModal: React.FC = if (selectedPrinterId === "") { setSnackbar({ open: true, - message: "請先選擇印表機", + message: t("Please select a printer first"), severity: "error", }); return; @@ -478,7 +480,7 @@ const WorkbenchLotLabelPrintModal: React.FC = if (printQty < 1 || !Number.isFinite(printQty)) { setSnackbar({ open: true, - message: "列印張數需為大於等於 1 的整數", + message: t("Print quantity must be an integer of 1 or more"), severity: "error", }); return; @@ -493,25 +495,25 @@ const WorkbenchLotLabelPrintModal: React.FC = }); setSnackbar({ open: true, - message: `已送出列印:Lot ${lotNo}`, + message: t("Print sent: Lot {{lotNo}}", { lotNo }), severity: "success", }); } catch (e) { setSnackbar({ open: true, - message: e instanceof Error ? e.message : "列印失敗", + message: e instanceof Error ? e.message : t("Print failed"), severity: "error", }); } finally { setPrintingLotLineId(null); } }, - [selectedPrinterId, printQty], + [selectedPrinterId, printQty, t], ); return ( - 批號標籤列印(提貨台) + {t("Lot label print (pick station)")} {statusTitleText ? ( @@ -548,13 +550,13 @@ const WorkbenchLotLabelPrintModal: React.FC = > setScanInput(e.target.value)} fullWidth size="small" error={!!scanError} - helperText={scanError || "掃描後按 Enter 或點「查詢」"} + helperText={scanError || t("Scan then press Enter or click Look up")} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); @@ -568,7 +570,7 @@ const WorkbenchLotLabelPrintModal: React.FC = onClick={() => void handleAnalyze()} disabled={analysisLoading || !scanInput.trim()} > - {analysisLoading ? : "查詢"} + {analysisLoading ? : t("Look up")} @@ -594,16 +596,16 @@ const WorkbenchLotLabelPrintModal: React.FC = sx={{ minWidth: 260 }} disabled={printersLoading} > - 印表機 + {t("Printer")}