| @@ -186,7 +186,7 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ 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<Props> = ({ defaultTabIndex = 0, printerCom | |||
| <Tabs | |||
| value={tab} | |||
| onChange={handleTabChange} | |||
| variant="scrollable" | |||
| scrollButtons="auto" | |||
| allowScrollButtonsMobile | |||
| sx={{ | |||
| width: "100%", | |||
| maxWidth: "100%", | |||
| minHeight: 56, | |||
| borderBottom: 1, | |||
| borderColor: "divider", | |||
| "& .MuiTabs-flexContainer": { | |||
| columnGap: 2, | |||
| rowGap: 1, | |||
| columnGap: 0.5, | |||
| }, | |||
| /* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */ | |||
| "& .MuiTab-root": { | |||
| overflow: "visible", | |||
| minWidth: "auto", | |||
| px: 2, | |||
| minHeight: 56, | |||
| minWidth: 72, | |||
| maxWidth: 120, | |||
| px: 1, | |||
| py: 0.75, | |||
| whiteSpace: "normal", | |||
| lineHeight: 1.2, | |||
| textAlign: "center", | |||
| }, | |||
| }} | |||
| > | |||
| @@ -297,7 +308,7 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ 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={ | |||
| <Tooltip | |||
| @@ -329,7 +340,12 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||
| <Typography | |||
| component="span" | |||
| variant="inherit" | |||
| sx={{ pr: etraIncompleteDopoCount > 0 ? 1 : 0 }} | |||
| sx={{ | |||
| pr: etraIncompleteDopoCount > 0 ? 1 : 0, | |||
| whiteSpace: "normal", | |||
| lineHeight: 1.2, | |||
| textAlign: "center", | |||
| }} | |||
| > | |||
| {t("Etra Pick Order Detail")} | |||
| </Typography> | |||
| @@ -341,8 +357,8 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||
| <Tab label={t("Finished Good Record")} value={2} /> | |||
| <Tab label={t("Finished Good Record (All)")} value={3} /> | |||
| <Tab label={t("Ticket Release Table")} value={4} /> | |||
| <Tab label={t("成品出倉出箱數量")} value={5} /> | |||
| <Tab label={t("送貨路線摘要")} value={6} /> | |||
| <Tab label={t("FG Carton Qty")} value={5} /> | |||
| <Tab label={t("Truck Routing Summary")} value={6} /> | |||
| </Tabs> | |||
| <TabPanel value={tab} index={0}> | |||
| @@ -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<WorkbenchReportOption[]>([]); | |||
| const [laneOptions, setLaneOptions] = useState<WorkbenchReportOption[]>([]); | |||
| 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 ( | |||
| <Box sx={{ maxWidth: 820 }}> | |||
| <Typography variant="h6" sx={{ mb: 2 }}> | |||
| 送貨路線摘要 (Workbench) | |||
| {t("Truck Routing Summary (Workbench)")} | |||
| </Typography> | |||
| <Stack direction={{ xs: "column", md: "row" }} spacing={2} sx={{ mb: 2 }}> | |||
| <TextField | |||
| select | |||
| fullWidth | |||
| label="2/F 或 4/F" | |||
| label={t("2/F or 4/F")} | |||
| value={storeId} | |||
| onChange={(e) => onStoreChange(e.target.value)} | |||
| > | |||
| {storeOptions.map((opt) => ( | |||
| <MenuItem key={opt.value} value={opt.value}> | |||
| {opt.label} | |||
| {displayOptionLabel(opt.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </TextField> | |||
| <TextField | |||
| select | |||
| fullWidth | |||
| label="車線" | |||
| label={t("Lane")} | |||
| value={truckLanceCode} | |||
| onChange={(e) => setTruckLanceCode(e.target.value)} | |||
| disabled={!storeId} | |||
| > | |||
| {laneOptions.map((opt) => ( | |||
| <MenuItem key={opt.value} value={opt.value}> | |||
| {opt.label} | |||
| {displayOptionLabel(opt.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </TextField> | |||
| <TextField | |||
| fullWidth | |||
| label="日期" | |||
| label={t("Date")} | |||
| type="date" | |||
| value={date} | |||
| InputLabelProps={{ shrink: true }} | |||
| @@ -146,7 +153,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||
| disabled={!canDownload} | |||
| onClick={onDownload} | |||
| > | |||
| {loading ? "生成中..." : "下載報告 (PDF)"} | |||
| {loading ? t("Generating...") : t("Download report (PDF)")} | |||
| </Button> | |||
| </Box> | |||
| ); | |||
| @@ -421,22 +421,30 @@ function isWorkbenchSourceLotExpired(lot: any): boolean { | |||
| return false; | |||
| } | |||
| function getWorkbenchSourceLotStatusSummary(lot: any): { | |||
| type PickOrderT = (key: string, options?: Record<string, unknown>) => 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, unknown>) => 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; | |||
| } | |||
| @@ -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<WorkbenchLotLabelPrintModalProps> = | |||
| submitQty = null, | |||
| onSubmitQtyChange, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const scanInputRef = useRef<HTMLInputElement | null>(null); | |||
| const [scanInput, setScanInput] = useState(""); | |||
| const [scanError, setScanError] = useState<string | null>(null); | |||
| @@ -210,8 +212,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| }); | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| if (selectedPrinterId === "") { | |||
| setSnackbar({ | |||
| open: true, | |||
| message: "請先選擇印表機", | |||
| message: t("Please select a printer first"), | |||
| severity: "error", | |||
| }); | |||
| return; | |||
| @@ -478,7 +480,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| 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<WorkbenchLotLabelPrintModalProps> = | |||
| }); | |||
| 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 ( | |||
| <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> | |||
| <DialogTitle>批號標籤列印(提貨台)</DialogTitle> | |||
| <DialogTitle>{t("Lot label print (pick station)")}</DialogTitle> | |||
| <DialogContent> | |||
| <Stack spacing={2} sx={{ mt: 1 }}> | |||
| {statusTitleText ? ( | |||
| @@ -548,13 +550,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| > | |||
| <TextField | |||
| inputRef={scanInputRef} | |||
| label="掃碼內容" | |||
| label={t("Scan content")} | |||
| value={scanInput} | |||
| onChange={(e) => 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<WorkbenchLotLabelPrintModalProps> = | |||
| onClick={() => void handleAnalyze()} | |||
| disabled={analysisLoading || !scanInput.trim()} | |||
| > | |||
| {analysisLoading ? <CircularProgress size={18} /> : "查詢"} | |||
| {analysisLoading ? <CircularProgress size={18} /> : t("Look up")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -578,7 +580,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| }} | |||
| disabled={analysisLoading} | |||
| > | |||
| 清除 | |||
| {t("Clear")} | |||
| </Button> | |||
| </Stack> | |||
| </> | |||
| @@ -594,16 +596,16 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| sx={{ minWidth: 260 }} | |||
| disabled={printersLoading} | |||
| > | |||
| <InputLabel>印表機</InputLabel> | |||
| <InputLabel>{t("Printer")}</InputLabel> | |||
| <Select | |||
| label="印表機" | |||
| label={t("Printer")} | |||
| value={selectedPrinterId} | |||
| onChange={(e) => | |||
| setSelectedPrinterId((e.target.value as number) ?? "") | |||
| } | |||
| > | |||
| <MenuItem value=""> | |||
| <em>{printersLoading ? "載入中..." : "請選擇"}</em> | |||
| <em>{printersLoading ? t("Loading") : t("Please select")}</em> | |||
| </MenuItem> | |||
| {printers.map((p) => ( | |||
| <MenuItem key={p.id} value={p.id}> | |||
| @@ -614,7 +616,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| </FormControl> | |||
| <TextField | |||
| label="列印張數" | |||
| label={t("Print copies")} | |||
| size="small" | |||
| type="number" | |||
| inputProps={{ min: 1, step: 1 }} | |||
| @@ -626,7 +628,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| {onWorkbenchScanPick ? ( | |||
| <TextField | |||
| label="提交數量" | |||
| label={t("Submit Qty")} | |||
| size="small" | |||
| type="number" | |||
| inputProps={{ min: 0, step: 1 }} | |||
| @@ -651,7 +653,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| {analysisLoading ? ( | |||
| <CircularProgress size={18} /> | |||
| ) : ( | |||
| "刷新批號清單" | |||
| t("Refresh lot list") | |||
| )} | |||
| </Button> | |||
| @@ -661,7 +663,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| color="text.secondary" | |||
| sx={{ ml: { md: "auto" } }} | |||
| > | |||
| 已選:{formatPrinterLabel(selectedPrinter)} | |||
| {t("Selected printer", { printer: formatPrinterLabel(selectedPrinter) })} | |||
| </Typography> | |||
| )} | |||
| </Stack> | |||
| @@ -669,12 +671,12 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| {analysis && ( | |||
| <Box> | |||
| <Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}> | |||
| 品號:{analysis.itemCode} {analysis.itemName} | |||
| {t("Item code name", { code: analysis.itemCode, name: analysis.itemName })} | |||
| </Typography> | |||
| {filteredLots.length === 0 ? ( | |||
| <Alert severity="warning"> | |||
| 找不到該樓層有可用批號(availableQty > 0)。 | |||
| {t("No available lots on this floor")} | |||
| </Alert> | |||
| ) : ( | |||
| <Stack spacing={1}> | |||
| @@ -717,14 +719,16 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| sx={{ fontWeight: lot._scanned ? 800 : 600 }} | |||
| > | |||
| Lot:{lot.lotNo} | |||
| {lot._scanned ? "(當前批次)" : ""} | |||
| {lot._scanned ? t(" (current lot)") : ""} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| 位置:{loc || "—"} | |||
| {t("Location with value", { location: loc || "—" })} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| 可用量:{Number(lot.availableQty).toLocaleString()}{" "} | |||
| 單位:{lot.uom || ""} | |||
| {t("Available qty with uom", { | |||
| qty: Number(lot.availableQty).toLocaleString(), | |||
| uom: lot.uom || "", | |||
| })} | |||
| </Typography> | |||
| </Box> | |||
| <Stack | |||
| @@ -747,7 +751,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| {isPrinting ? ( | |||
| <CircularProgress size={18} /> | |||
| ) : ( | |||
| "列印標籤" | |||
| t("Print label") | |||
| )} | |||
| </Button> | |||
| {onWorkbenchScanPick ? ( | |||
| @@ -756,9 +760,9 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| color="secondary" | |||
| title={ | |||
| !lotQrPayload | |||
| ? "此列無法取得 QR payload(需 stockInLineId)" | |||
| ? t("This row has no QR payload") | |||
| : disableScanPick | |||
| ? "此出庫行已掃碼或已完成,無法顯示 QR" | |||
| ? t("This pick line already scanned or completed, QR cannot be shown") | |||
| : undefined | |||
| } | |||
| disabled={ | |||
| @@ -772,7 +776,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| ) | |||
| } | |||
| > | |||
| 顯示 QR | |||
| {t("Show QR")} | |||
| </Button> | |||
| ) : null} | |||
| </Stack> | |||
| @@ -809,14 +813,14 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {onWorkbenchScanPick | |||
| ? "沒有任何批號可列印標籤" | |||
| ? t("No lots available to print labels") | |||
| : ""} | |||
| </Typography> | |||
| )} | |||
| </Stack> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={onClose}>關閉</Button> | |||
| <Button onClick={onClose}>{t("Close")}</Button> | |||
| </DialogActions> | |||
| <Snackbar | |||
| @@ -28,6 +28,7 @@ import { | |||
| fetchCompletedDoPickOrdersWorkbenchAll, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import SafeApexCharts from "@/components/charts/SafeApexCharts"; | |||
| import { useTranslation } from "react-i18next"; | |||
| type FloorFilter = "all" | "2/F" | "4/F"; | |||
| @@ -44,6 +45,8 @@ type Props = { | |||
| }; | |||
| const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => { | |||
| const { t, i18n } = useTranslation(); | |||
| const numberLocale = i18n.language?.startsWith("zh") ? "zh-HK" : "en-US"; | |||
| const [floor, setFloor] = useState<FloorFilter>("all"); | |||
| const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD")); | |||
| const [loading, setLoading] = useState(false); | |||
| @@ -66,12 +69,12 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| setRecords(data); | |||
| } catch (err) { | |||
| console.error("Failed to load finished good carton dashboard data", err); | |||
| setError("載入成品出倉出箱數量失敗,請稍後再試。"); | |||
| setError(t("Failed to load FG carton quantity. Please try again later.")); | |||
| setRecords([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [date, mode]); | |||
| }, [date, mode, t]); | |||
| useEffect(() => { | |||
| loadData(); | |||
| @@ -132,37 +135,40 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| }, | |||
| xaxis: { | |||
| categories: rows.map((row) => row.date), | |||
| title: { text: "日期" }, | |||
| title: { text: t("Date") }, | |||
| }, | |||
| yaxis: { | |||
| title: { text: "箱數" }, | |||
| title: { text: t("Cartons") }, | |||
| labels: { | |||
| formatter: (val) => Number(val || 0).toLocaleString("zh-HK"), | |||
| formatter: (val) => Number(val || 0).toLocaleString(numberLocale), | |||
| }, | |||
| }, | |||
| tooltip: { | |||
| y: { | |||
| formatter: (val) => `${Number(val || 0).toLocaleString("zh-HK")} 箱`, | |||
| formatter: (val) => | |||
| t("{{count}} cartons", { | |||
| count: Number(val || 0).toLocaleString(numberLocale), | |||
| }), | |||
| }, | |||
| }, | |||
| legend: { | |||
| position: "top", | |||
| }, | |||
| noData: { | |||
| text: "沒有圖表資料", | |||
| text: t("No chart data"), | |||
| }, | |||
| }), | |||
| [rows], | |||
| [rows, t, numberLocale], | |||
| ); | |||
| const chartSeries = useMemo( | |||
| () => [ | |||
| { name: "2/F", data: rows.map((row) => row.floor2F) }, | |||
| { name: "4/F", data: rows.map((row) => row.floor4F) }, | |||
| { name: "車線-X", data: rows.map((row) => row.truckX) }, | |||
| { name: "總數", data: rows.map((row) => row.total) }, | |||
| { name: t("Truck X"), data: rows.map((row) => row.truckX) }, | |||
| { name: t("Total"), data: rows.map((row) => row.total) }, | |||
| ], | |||
| [rows], | |||
| [rows, t], | |||
| ); | |||
| const summary = useMemo(() => { | |||
| @@ -317,21 +323,27 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| const aoa: (string | number)[][] = [ | |||
| [reportTitle, "", "", "", ""], | |||
| ["", "", "", "", ""], | |||
| ["日期", "2/F 出箱數", "4/F 出箱數", "車線-X 出箱數", "總出箱數"], | |||
| [ | |||
| t("Date"), | |||
| t("2/F carton qty"), | |||
| t("4/F carton qty"), | |||
| t("Truck X carton qty"), | |||
| t("Total carton qty"), | |||
| ], | |||
| ...dailyRows.map((row) => [row.date, row.floor2F, row.floor4F, row.truckX, row.total]), | |||
| ["", "", "", "", ""], | |||
| ["彙總", "", "", "", ""], | |||
| ["2/F 出箱數", reportSummary.floor2F, "", "", ""], | |||
| ["4/F 出箱數", reportSummary.floor4F, "", "", ""], | |||
| ["車線-X 出箱數", reportSummary.truckX, "", "", ""], | |||
| ["總出箱數", reportSummary.total, "", "", ""], | |||
| [t("Summary"), "", "", "", ""], | |||
| [t("2/F carton qty"), reportSummary.floor2F, "", "", ""], | |||
| [t("4/F carton qty"), reportSummary.floor4F, "", "", ""], | |||
| [t("Truck X carton qty"), reportSummary.truckX, "", "", ""], | |||
| [t("Total carton qty"), reportSummary.total, "", "", ""], | |||
| ]; | |||
| const worksheet = XLSX.utils.aoa_to_sheet(aoa); | |||
| styleWorksheet(worksheet, dailyRows.length); | |||
| XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); | |||
| }, | |||
| [calcSummary, styleWorksheet], | |||
| [calcSummary, styleWorksheet, t], | |||
| ); | |||
| const handleDownloadExcel = useCallback(async () => { | |||
| @@ -343,8 +355,14 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| : await fetchCompletedDoPickOrdersAll(); | |||
| const baseDate = dayjs(date || undefined).isValid() ? dayjs(date) : dayjs(); | |||
| const floorLabel = floor === "all" ? "全部樓層" : floor; | |||
| const floorLabel = floor === "all" ? t("All floors") : floor; | |||
| const dateLabel = baseDate.format("YYYY-MM-DD"); | |||
| const monthPeriod = i18n.language?.startsWith("zh") | |||
| ? baseDate.format("YYYY年MM月") | |||
| : baseDate.format("YYYY-MM"); | |||
| const yearPeriod = i18n.language?.startsWith("zh") | |||
| ? baseDate.format("YYYY年") | |||
| : baseDate.format("YYYY"); | |||
| const last7Rows = buildDailyRowsFromRecords( | |||
| allRecords, | |||
| @@ -368,39 +386,42 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| const workbook = XLSX.utils.book_new(); | |||
| addReportSheet( | |||
| workbook, | |||
| "最近7天", | |||
| `成品出倉出箱數量(最近7天)- ${floorLabel} - 基準日 ${dateLabel}`, | |||
| t("Last 7 days"), | |||
| t("FG carton qty last 7 days title", { floor: floorLabel, date: dateLabel }), | |||
| last7Rows, | |||
| ); | |||
| addReportSheet( | |||
| workbook, | |||
| "本月", | |||
| `成品出倉出箱數量(本月)- ${floorLabel} - ${baseDate.format("YYYY年MM月")}`, | |||
| t("This month"), | |||
| t("FG carton qty this month title", { floor: floorLabel, period: monthPeriod }), | |||
| monthRows, | |||
| ); | |||
| addReportSheet( | |||
| workbook, | |||
| "本年", | |||
| `成品出倉出箱數量(本年)- ${floorLabel} - ${baseDate.format("YYYY年")}`, | |||
| t("This year"), | |||
| t("FG carton qty this year title", { floor: floorLabel, period: yearPeriod }), | |||
| yearRows, | |||
| ); | |||
| XLSX.writeFile(workbook, `成品出倉出箱數量_多時段報表_${floorLabel.replace("/", "")}_${dateLabel}.xlsx`); | |||
| XLSX.writeFile( | |||
| workbook, | |||
| `FG_carton_qty_${floorLabel.replace("/", "")}_${dateLabel}.xlsx`, | |||
| ); | |||
| } finally { | |||
| setIsExporting(false); | |||
| } | |||
| }, [mode, date, floor, buildDailyRowsFromRecords, addReportSheet]); | |||
| }, [mode, date, floor, buildDailyRowsFromRecords, addReportSheet, t, i18n.language]); | |||
| return ( | |||
| <Box sx={{ width: "100%" }}> | |||
| <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}> | |||
| <Typography variant="h6">成品出倉出箱數量</Typography> | |||
| <Typography variant="h6">{t("FG Carton Qty")}</Typography> | |||
| <Button | |||
| variant="contained" | |||
| onClick={handleDownloadExcel} | |||
| disabled={loading || isExporting} | |||
| > | |||
| {isExporting ? "匯出中..." : "下載 Excel"} | |||
| {isExporting ? t("Exporting...") : t("Download Excel")} | |||
| </Button> | |||
| </Stack> | |||
| @@ -421,11 +442,11 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| <TextField | |||
| select | |||
| fullWidth | |||
| label="樓層" | |||
| label={t("Floor")} | |||
| value={floor} | |||
| onChange={(event) => setFloor(event.target.value as FloorFilter)} | |||
| > | |||
| <MenuItem value="all">全部</MenuItem> | |||
| <MenuItem value="all">{t("All")}</MenuItem> | |||
| <MenuItem value="2/F">2/F</MenuItem> | |||
| <MenuItem value="4/F">4/F</MenuItem> | |||
| </TextField> | |||
| @@ -433,7 +454,7 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| <Grid item xs={12} md={6}> | |||
| <TextField | |||
| fullWidth | |||
| label="日期" | |||
| label={t("Date")} | |||
| type="date" | |||
| value={date} | |||
| InputLabelProps={{ shrink: true }} | |||
| @@ -448,20 +469,20 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| <Table size="small"> | |||
| <TableBody> | |||
| <TableRow> | |||
| <TableCell>2/F 出箱數</TableCell> | |||
| <TableCell align="right">{summary.floor2F.toLocaleString("zh-HK")}</TableCell> | |||
| <TableCell>{t("2/F carton qty")}</TableCell> | |||
| <TableCell align="right">{summary.floor2F.toLocaleString(numberLocale)}</TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell>4/F 出箱數</TableCell> | |||
| <TableCell align="right">{summary.floor4F.toLocaleString("zh-HK")}</TableCell> | |||
| <TableCell>{t("4/F carton qty")}</TableCell> | |||
| <TableCell align="right">{summary.floor4F.toLocaleString(numberLocale)}</TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell>車線-X 出箱數</TableCell> | |||
| <TableCell align="right">{summary.truckX.toLocaleString("zh-HK")}</TableCell> | |||
| <TableCell>{t("Truck X carton qty")}</TableCell> | |||
| <TableCell align="right">{summary.truckX.toLocaleString(numberLocale)}</TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell>總出箱數</TableCell> | |||
| <TableCell align="right">{summary.total.toLocaleString("zh-HK")}</TableCell> | |||
| <TableCell>{t("Total carton qty")}</TableCell> | |||
| <TableCell align="right">{summary.total.toLocaleString(numberLocale)}</TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| @@ -654,11 +654,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| const reminder = workbenchLotLabelReminderText?.trim() ?? ""; | |||
| if (reminder && isExpiredWorkbenchReminderMessage(reminder)) { | |||
| return { text: "此批號狀態:已過期", severity: "error" as const }; | |||
| return { text: t("Lot status: expired"), severity: "error" as const }; | |||
| } | |||
| const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot); | |||
| const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot, t); | |||
| return { text: s.text, severity: s.severity }; | |||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, workbenchLotLabelReminderText]); | |||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, workbenchLotLabelReminderText, t]); | |||
| const handleJustComplete = useCallback( | |||
| async (row: LotRow) => { | |||
| @@ -1714,7 +1714,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| onClick={() => openWorkbenchLotLabelModalForLot(r)} | |||
| sx={{ flexShrink: 0, fontSize: "0.7rem", py: 0.25, minWidth: "auto", px: 1, whiteSpace: "nowrap" }} | |||
| > | |||
| {t(" 批號 QR 碼")} | |||
| {t("lot QR code")} | |||
| </Button> | |||
| ) : null} | |||
| </Stack> | |||
| @@ -1,131 +1,131 @@ | |||
| { | |||
| "Actions": "操作", | |||
| "Add Document": "新增文件", | |||
| "Actions": "Actions", | |||
| "Add Document": "Add Document", | |||
| "All": "All", | |||
| "Allergic Substances": "過敏原", | |||
| "Allergic Substances": "Allergens", | |||
| "An error has occurred. Please try again later.": "An error has occurred. Please try again later.", | |||
| "Are you sure you want to delete this item?": "您確定要刪除此項目嗎?", | |||
| "Back": "返回", | |||
| "Basic Info": "基本資訊", | |||
| "Are you sure you want to delete this item?": "Are you sure you want to delete this item?", | |||
| "Back": "Back", | |||
| "Basic Info": "Basic Info", | |||
| "Bom Required Qty": "BOM Required Qty", | |||
| "Bom UOM": "BOM UOM", | |||
| "Brand": "品牌", | |||
| "CMB": "消耗品", | |||
| "CO": "消耗品", | |||
| "Cancel": "取消", | |||
| "Column Name": "欄位名稱", | |||
| "Coming soon": "即將推出", | |||
| "Complexity": "複雜度", | |||
| "Confirm": "確認", | |||
| "Confirm Delete": "確認刪除", | |||
| "Cost (HKD)": "費用 (HKD)", | |||
| "Current total": "目前總和", | |||
| "Day Before Yesterday": "前天", | |||
| "Delete": "刪除", | |||
| "Delete Failed": "刪除失敗", | |||
| "Brand": "Brand", | |||
| "CMB": "Consumable", | |||
| "CO": "Consumable", | |||
| "Cancel": "Cancel", | |||
| "Column Name": "Column Name", | |||
| "Coming soon": "Coming soon", | |||
| "Complexity": "Complexity", | |||
| "Confirm": "Confirm", | |||
| "Confirm Delete": "Confirm Delete", | |||
| "Cost (HKD)": "Cost (HKD)", | |||
| "Current total": "Current total", | |||
| "Day Before Yesterday": "Day Before Yesterday", | |||
| "Delete": "Delete", | |||
| "Delete Failed": "Delete Failed", | |||
| "Do you want to delete?": "Do you want to delete?", | |||
| "Density": "濃淡", | |||
| "Depth": "顔色深淺度 深1淺5", | |||
| "Description": "描述", | |||
| "Details": "詳情", | |||
| "Duration (Minutes)": "時間(分)", | |||
| "Edit": "編輯", | |||
| "Enter any additional observations or notes...": "輸入其他觀察或備註...", | |||
| "Enter or select remark": "輸入或選擇備註", | |||
| "Density": "Density", | |||
| "Depth": "Color depth (dark 1 / light 5)", | |||
| "Description": "Description", | |||
| "Details": "Details", | |||
| "Duration (Minutes)": "Duration (Minutes)", | |||
| "Edit": "Edit", | |||
| "Enter any additional observations or notes...": "Enter any additional observations or notes...", | |||
| "Enter or select remark": "Enter or select remark", | |||
| "Error saving data": "Error saving data", | |||
| "Failed to fetch data": "無法取得資料", | |||
| "Filter": "過濾", | |||
| "Finished Good Detail": "成品出倉詳情", | |||
| "Finished Good Management": "成品出倉管理", | |||
| "Finished Good Order": "成品出倉", | |||
| "Float": "浮沉", | |||
| "General Data": "基本資料", | |||
| "Grade {{grade}}": "等級 {{grade}}", | |||
| "Invoice": "發票", | |||
| "Invoice Date": "發票日期", | |||
| "Failed to fetch data": "Failed to fetch data", | |||
| "Filter": "Filter", | |||
| "Finished Good Detail": "Finished Good Detail", | |||
| "Finished Good Management": "Finished Good Management", | |||
| "Finished Good Order": "Finished Good Order", | |||
| "Float": "Float", | |||
| "General Data": "General Data", | |||
| "Grade {{grade}}": "Grade {{grade}}", | |||
| "Invoice": "Invoice", | |||
| "Invoice Date": "Invoice Date", | |||
| "IP": "IP", | |||
| "Item Code": "Item Code", | |||
| "Item Name": "Item Name", | |||
| "Loading": "載入中...", | |||
| "Loading order summary": "正在載入訂單摘要", | |||
| "Location": "位置", | |||
| "MA": "材料", | |||
| "MAT": "材料", | |||
| "MI": "雜項", | |||
| "Material Name": "材料清單", | |||
| "Loading": "Loading...", | |||
| "Loading order summary": "Loading order summary", | |||
| "Location": "Location", | |||
| "MA": "Material", | |||
| "MAT": "Material", | |||
| "MI": "Miscellaneous", | |||
| "Material Name": "Material List", | |||
| "Name": "Name", | |||
| "Min": "最小值", | |||
| "NM": "雜項及非消耗品", | |||
| "No": "否", | |||
| "No Lot": "沒有批號", | |||
| "No data available": "沒有資料", | |||
| "No options": "沒有選項", | |||
| "Order": "順序", | |||
| "Min": "Min", | |||
| "NM": "Miscellaneous and non-consumables", | |||
| "No": "No", | |||
| "No Lot": "No Lot", | |||
| "No data available": "No data available", | |||
| "No options": "No options", | |||
| "Order": "Order", | |||
| "Pending": "Pending", | |||
| "Port": "Port", | |||
| "Please Select BOM": "請選擇 BOM", | |||
| "Please try again later.": "請稍後重試。", | |||
| "Project Code": "專案代碼", | |||
| "Project Code and Name": "專案代碼與名稱", | |||
| "QC Template not found": "找不到 QC 範本", | |||
| "Qty": "數量", | |||
| "RM": "原料", | |||
| "Range": "範圍", | |||
| "Refresh": "重新載入", | |||
| "Remarks": "備註", | |||
| "Remove Document": "移除文件", | |||
| "Report": "報告", | |||
| "Please Select BOM": "Please select BOM", | |||
| "Please try again later.": "Please try again later.", | |||
| "Project Code": "Project Code", | |||
| "Project Code and Name": "Project Code and Name", | |||
| "QC Template not found": "QC template not found", | |||
| "Qty": "Qty", | |||
| "RM": "Raw material", | |||
| "Range": "Range", | |||
| "Refresh": "Refresh", | |||
| "Remarks": "Remarks", | |||
| "Remove Document": "Remove Document", | |||
| "Report": "Report", | |||
| "Reset": "Reset", | |||
| "Row per page": "每頁行數", | |||
| "Rows per page": "每頁行數", | |||
| "Sales Qty": "銷售數量", | |||
| "Sales UOM": "銷售單位", | |||
| "Save": "儲存", | |||
| "Saving": "儲存中", | |||
| "Row per page": "Rows per page", | |||
| "Rows per page": "Rows per page", | |||
| "Sales Qty": "Sales Qty", | |||
| "Sales UOM": "Sales UOM", | |||
| "Save": "Save", | |||
| "Saving": "Saving", | |||
| "Search": "Search", | |||
| "Search Criteria": "Search Criteria", | |||
| "Select Date": "選擇日期", | |||
| "Session expired or unauthorized.": "工作階段已過期或未經授權。", | |||
| "Select Date": "Select Date", | |||
| "Session expired or unauthorized.": "Session expired or unauthorized.", | |||
| "Sign out": "Sign out", | |||
| "Language": "Language", | |||
| "Status": "狀態", | |||
| "Stock Qty": "庫存數量", | |||
| "Supporting Document": "證明文件", | |||
| "Task": "任務", | |||
| "Time Sequence": "時段", | |||
| "Status": "Status", | |||
| "Stock Qty": "Stock Qty", | |||
| "Supporting Document": "Supporting Document", | |||
| "Task": "Task", | |||
| "Time Sequence": "Time Sequence", | |||
| "Type": "Type", | |||
| "Today": "今天", | |||
| "Total weighting must equal 1": "權重總和必須等於 1", | |||
| "Unauthorized: Please log in again": "未經授權:請重新登入", | |||
| "Uom": "單位", | |||
| "Update Failed": "更新失敗", | |||
| "Update Success": "更新成功", | |||
| "Weighting must be a number": "權重必須為數字", | |||
| "Yes": "是", | |||
| "Yesterday": "昨天", | |||
| "all": "全部", | |||
| "Today": "Today", | |||
| "Total weighting must equal 1": "Total weighting must equal 1", | |||
| "Unauthorized: Please log in again": "Unauthorized: Please log in again", | |||
| "Uom": "UOM", | |||
| "Update Failed": "Update Failed", | |||
| "Update Success": "Update Success", | |||
| "Weighting must be a number": "Weighting must be a number", | |||
| "Yes": "Yes", | |||
| "Yesterday": "Yesterday", | |||
| "all": "All", | |||
| "bomWeighting": "BOM Weighting Score", | |||
| "cmb": "消耗品", | |||
| "collapsible table": "可折疊表格", | |||
| "consumable": "消耗品", | |||
| "consumables": "消耗品", | |||
| "create": "新增", | |||
| "edit": "編輯", | |||
| "expand row": "展開行", | |||
| "group mode": "群組模式", | |||
| "item": "貨品", | |||
| "items": "物品", | |||
| "mat": "原料", | |||
| "menu": "選單", | |||
| "nm": "雜項及非消耗品", | |||
| "non-consumables": "非消耗品", | |||
| "other": "其他", | |||
| "profile": "個人資料", | |||
| "revert": "還原", | |||
| "settings": "設定", | |||
| "stockRecord": "盤點記錄", | |||
| "stocktakemanagement": "盤點管理", | |||
| "testing sections tabs": "測試區域分頁", | |||
| "warehouse": "倉庫", | |||
| "材料": "材料" | |||
| "cmb": "Consumable", | |||
| "collapsible table": "Collapsible table", | |||
| "consumable": "Consumable", | |||
| "consumables": "Consumables", | |||
| "create": "Create", | |||
| "edit": "Edit", | |||
| "expand row": "Expand row", | |||
| "group mode": "Group mode", | |||
| "item": "Item", | |||
| "items": "Items", | |||
| "mat": "Raw material", | |||
| "menu": "Menu", | |||
| "nm": "Miscellaneous and non-consumables", | |||
| "non-consumables": "Non-consumables", | |||
| "other": "Other", | |||
| "profile": "Profile", | |||
| "revert": "Revert", | |||
| "settings": "Settings", | |||
| "stockRecord": "Stock record", | |||
| "stocktakemanagement": "Stock take management", | |||
| "testing sections tabs": "Testing section tabs", | |||
| "warehouse": "Warehouse", | |||
| "材料": "Material" | |||
| } | |||
| @@ -45,5 +45,24 @@ | |||
| "DO Workbench Search": "DO Workbench Search", | |||
| "completed": "completed", | |||
| "items": "items", | |||
| "pending": "pending" | |||
| "pending": "pending", | |||
| "Pick Order Detail": "Pick Detail", | |||
| "Etra Pick Order Detail": "Etra", | |||
| "Finished Good Record": "FG Record", | |||
| "Finished Good Record (All)": "FG Record (All)", | |||
| "Ticket Release Table": "Ticket Release", | |||
| "FG Carton Qty": "Carton Qty", | |||
| "成品出倉出箱數量": "Carton Qty", | |||
| "Truck Routing Summary": "Routing Summary", | |||
| "送貨路線摘要": "Truck Routing Summary", | |||
| "車線-X": "Truck X", | |||
| "Confirm print drafts": "Print {{count}} draft(s)?", | |||
| "Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)", | |||
| "2/F or 4/F": "2/F or 4/F", | |||
| "Lane": "Lane", | |||
| "Date": "Date", | |||
| "Generating...": "Generating...", | |||
| "Download report (PDF)": "Download report (PDF)", | |||
| "Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?", | |||
| "Failed to download Workbench truck routing summary. Please try again later.": "Failed to download Workbench truck routing summary. Please try again later." | |||
| } | |||
| @@ -58,7 +58,7 @@ | |||
| "Lines": "Lines", | |||
| "Before Today": "Before Today", | |||
| "Truck X": "Truck X", | |||
| "Finsihed good items": "Finsihed good items", | |||
| "Finsihed good items": "Finished good items", | |||
| "kinds": "kinds", | |||
| "Completed Date": "Completed Date", | |||
| "Completed Time": "Completed Time", | |||
| @@ -147,8 +147,8 @@ | |||
| "Etra": "Etra", | |||
| "Exit Etra view": "Exit Etra view", | |||
| "Etra Pick Order Detail": "Etra Pick Order Detail", | |||
| "Etra incomplete badge tooltip": "Etra incomplete badge tooltip", | |||
| "Etra incomplete badge tooltip none": "Etra incomplete badge tooltip none", | |||
| "Etra incomplete badge tooltip": "Incomplete extra tickets today: {{count}} (pending/released, excluding completed)", | |||
| "Etra incomplete badge tooltip none": "No incomplete extra tickets", | |||
| "Back to normal assign tab": "Back to normal assign tab", | |||
| "Enter isExtra workbench view?": "Enter isExtra workbench view?", | |||
| "Etra view groups all add-on tickets by shop and lane for the selected date.": "Etra view groups all add-on tickets by shop and lane for the selected date.", | |||
| @@ -179,7 +179,6 @@ | |||
| "Confirm Search": "Search", | |||
| "Merge Etra ticket search prompt": "Enter shop (optional) and date, then click Search to load merge candidates.", | |||
| "Merge Etra ticket search failed": "Failed to load merge candidates. Ensure the backend is updated and restarted.", | |||
| "Truck X": "Truck X", | |||
| "Pick Order": "Pick Order", | |||
| "Type": "Type", | |||
| "Product Type": "Product Type", | |||
| @@ -529,14 +528,14 @@ | |||
| "Floor ticket": "Floor ticket", | |||
| "2F ticket": "2F ticket", | |||
| "4F ticket": "4F ticket", | |||
| "4F lane panel legend": "4F lane panel legend", | |||
| "Loading sequence n": "Loading sequence n", | |||
| "lot QR code": "lot QR code", | |||
| "4F lane panel legend": "Lane — loading sequence (unassigned/total)", | |||
| "Loading sequence n": "Board {{n}}", | |||
| "lot QR code": "Lot QR Code", | |||
| "label Printer": "label Printer", | |||
| "Loading Sequence": "Loading Sequence", | |||
| "Ticket No": "Ticket No", | |||
| "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "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.": "is unavable. Please check around have available QR code or not.", | |||
| "is unavable. Please check around have available QR code or not.": "This lot is unavailable. Please check around for an available QR code.", | |||
| "Lot switch failed; pick line was not marked as checked.": "Lot switch failed; pick line was not marked as checked.", | |||
| "Lot confirmation failed. Please try again.": "Lot confirmation failed. Please try again.", | |||
| "Powder Mixture": "Powder Mixture", | |||
| @@ -559,5 +558,80 @@ | |||
| "passed": "Passed", | |||
| "failed": "Failed", | |||
| "confirm_accept_with_fail": "There are failed QC items. Confirm to accept stock out?", | |||
| "No pending pick line left for this item. It may already be completed or fully processed.": "No pending pick line left for this item. It may already be completed or fully processed." | |||
| "No pending pick line left for this item. It may already be completed or fully processed.": "No pending pick line left for this item. It may already be completed or fully processed.", | |||
| "FG Carton Qty": "FG Carton Qty", | |||
| "成品出倉出箱數量": "FG Carton Qty", | |||
| "Truck Routing Summary": "Truck Routing Summary", | |||
| "送貨路線摘要": "Truck Routing Summary", | |||
| "車線-X": "Truck X", | |||
| "Lot QR Code": "Lot QR Code", | |||
| " 批號 QR 碼": "Lot QR Code", | |||
| "Cannot determine this lot status": "Cannot determine this lot status", | |||
| "Lot status: expired": "Lot status: expired", | |||
| "This pick line was rejected. Please scan another lot.": "This pick line was rejected. Please scan another lot.", | |||
| "This pick line is already completed. No further pick needed.": "This pick line is already completed. No further pick needed.", | |||
| "No lot bound / no available stock line. Please scan nearby inbound or transfer QR.": "No lot bound / no available stock line. Please scan nearby inbound or transfer QR.", | |||
| "Lot status: depleted (no remaining stock)": "Lot status: depleted (no remaining stock)", | |||
| "Lot status: depleted (available qty is 0)": "Lot status: depleted (available qty is 0)", | |||
| "Lot status: unavailable (not put away or line unavailable)": "Lot status: unavailable (not put away or line unavailable)", | |||
| "Lot status: ready to pick": "Lot status: ready to pick", | |||
| "This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.": "This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.", | |||
| "No lot information for this item in the current order": "No lot information for this item in the current order", | |||
| "Lot label print (pick station)": "Lot label print (pick station)", | |||
| "Scan content": "Scan content", | |||
| "Scan then press Enter or click Look up": "Scan then press Enter or click Look up", | |||
| "Look up": "Look up", | |||
| "Clear": "Clear", | |||
| "Printer": "Printer", | |||
| "Please select": "Please select", | |||
| "Print copies": "Print copies", | |||
| "Refresh lot list": "Refresh lot list", | |||
| "Selected printer": "Selected: {{printer}}", | |||
| "Item code name": "Item: {{code}} {{name}}", | |||
| "No available lots on this floor": "No available lots on this floor (available qty > 0).", | |||
| " (current lot)": " (current lot)", | |||
| "Location with value": "Location: {{location}}", | |||
| "Available qty with uom": "Available qty: {{qty}} UOM: {{uom}}", | |||
| "Print label": "Print label", | |||
| "Show QR": "Show QR", | |||
| "This row has no QR payload": "This row has no QR payload (stockInLineId required)", | |||
| "This pick line already scanned or completed, QR cannot be shown": "This pick line is already scanned or completed, so QR cannot be shown", | |||
| "No lots available to print labels": "No lots available to print labels", | |||
| "Failed to load printer list": "Failed to load printer list", | |||
| "Loaded available lots for this item": "Loaded available lots for this item", | |||
| "Analysis failed": "Look-up failed", | |||
| "Invalid itemId, cannot load lot list.": "Invalid item ID, cannot load lot list.", | |||
| "Invalid scan format. Please scan again.": "Invalid scan format. Please scan again.", | |||
| "Scan or look up once before refreshing the lot list.": "Scan or look up once before refreshing the lot list.", | |||
| "Print quantity must be an integer of 1 or more": "Print quantity must be an integer of 1 or more", | |||
| "Print sent: Lot {{lotNo}}": "Print sent: Lot {{lotNo}}", | |||
| "Print failed": "Print failed", | |||
| "Failed to load FG carton quantity. Please try again later.": "Failed to load FG carton quantity. Please try again later.", | |||
| "Cartons": "Cartons", | |||
| "{{count}} cartons": "{{count}} cartons", | |||
| "No chart data": "No chart data", | |||
| "2/F carton qty": "2/F carton qty", | |||
| "4/F carton qty": "4/F carton qty", | |||
| "Truck X carton qty": "Truck X carton qty", | |||
| "Total carton qty": "Total carton qty", | |||
| "Summary": "Summary", | |||
| "All floors": "All floors", | |||
| "Last 7 days": "Last 7 days", | |||
| "This month": "This month", | |||
| "This year": "This year", | |||
| "FG carton qty last 7 days title": "FG carton qty (last 7 days) - {{floor}} - as of {{date}}", | |||
| "FG carton qty this month title": "FG carton qty (this month) - {{floor}} - {{period}}", | |||
| "FG carton qty this year title": "FG carton qty (this year) - {{floor}} - {{period}}", | |||
| "Exporting...": "Exporting...", | |||
| "Download Excel": "Download Excel", | |||
| "Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)", | |||
| "2/F or 4/F": "2/F or 4/F", | |||
| "Lane": "Lane", | |||
| "Generating...": "Generating...", | |||
| "Download report (PDF)": "Download report (PDF)", | |||
| "Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?", | |||
| "Failed to download Workbench truck routing summary. Please try again later.": "Failed to download Workbench truck routing summary. Please try again later.", | |||
| "Confirm print drafts": "Print {{count}} draft(s)?", | |||
| "Floor": "Floor", | |||
| "All": "All" | |||
| } | |||
| @@ -16,11 +16,11 @@ | |||
| "Departure Time": "Departure Time", | |||
| "Floor": "Floor", | |||
| "Force complete DO": "Force complete DO", | |||
| "Force complete hint": "Force complete hint", | |||
| "Force complete hint": "Marks the ticket completed and archived without changing picked quantities. Use when all lines are submitted but the system did not complete.", | |||
| "Handler Name": "Handler Name", | |||
| "Last updated": "Last updated", | |||
| "Loading Sequence": "Loading Sequence", | |||
| "Manager only hint": "Manager only hint", | |||
| "Manager only hint": "Admin only", | |||
| "No data available": "No data available", | |||
| "Now": "Now", | |||
| "Number of FG Items (Order Item(s) Count)": "Number of FG Items (Order Item(s) Count)", | |||
| @@ -29,7 +29,7 @@ | |||
| "Reload data": "Reload data", | |||
| "Required Delivery Date": "Required Delivery Date", | |||
| "Revert assignment": "Revert assignment", | |||
| "Revert assignment hint": "Revert assignment hint", | |||
| "Revert assignment hint": "Clears the assigned handler so the ticket returns to unassigned and can be taken again.", | |||
| "Rows per page": "Rows per page", | |||
| "Select All": "Select All", | |||
| "Select Date": "Select Date", | |||
| @@ -45,5 +45,19 @@ | |||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | |||
| "Last updated": "最後更新", | |||
| "Truck Information": "車線資訊", | |||
| "Actions": "操作" | |||
| "Actions": "操作", | |||
| "FG Carton Qty": "成品出倉出箱數量", | |||
| "成品出倉出箱數量": "成品出倉出箱數量", | |||
| "Truck Routing Summary": "送貨路線摘要", | |||
| "送貨路線摘要": "送貨路線摘要", | |||
| "車線-X": "車線-X", | |||
| "Confirm print drafts": "確認列印 {{count}} 張草稿?", | |||
| "Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)", | |||
| "2/F or 4/F": "2/F 或 4/F", | |||
| "Lane": "車線", | |||
| "Date": "日期", | |||
| "Generating...": "生成中...", | |||
| "Download report (PDF)": "下載報告 (PDF)", | |||
| "Unpicked orders confirm download": "此車線仍有 {{count}} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?", | |||
| "Failed to download Workbench truck routing summary. Please try again later.": "下載 Workbench 送貨路線摘要失敗,請稍後再試。" | |||
| } | |||
| @@ -568,5 +568,82 @@ | |||
| "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 pending pick line left for this item. It may already be completed or fully processed.": "此貨品已無待揀行,可能已完成或不在本單可掃範圍。" | |||
| } | |||
| "No pending pick line left for this item. It may already be completed or fully processed.": "此貨品已無待揀行,可能已完成或不在本單可掃範圍。", | |||
| "FG Carton Qty": "成品出倉出箱數量", | |||
| "成品出倉出箱數量": "成品出倉出箱數量", | |||
| "Truck Routing Summary": "送貨路線摘要", | |||
| "送貨路線摘要": "送貨路線摘要", | |||
| "車線-X": "車線-X", | |||
| "Lot QR Code": "批號 QR 碼", | |||
| " 批號 QR 碼": "批號 QR 碼", | |||
| "Cannot determine this lot status": "無法判斷此批號狀態", | |||
| "Lot status: expired": "此批號狀態:已過期", | |||
| "This pick line was rejected. Please scan another lot.": "此出庫行:已拒絕,請改掃其他批號", | |||
| "This pick line is already completed. No further pick needed.": "此出庫行:已完成,無需再提貨", | |||
| "No lot bound / no available stock line. Please scan nearby inbound or transfer QR.": "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR", | |||
| "Lot status: depleted (no remaining stock)": "此批號狀態:已用畢(無剩餘庫存)", | |||
| "Lot status: depleted (available qty is 0)": "此批號狀態:已用畢(可用量為 0)", | |||
| "Lot status: unavailable (not put away or line unavailable)": "此批號狀態:庫存不可用(未上架或行狀態不可用)", | |||
| "Lot status: ready to pick": "此批號狀態:可提貨", | |||
| "This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.": "此批次({{lot}})已被拒絕,無法使用。請掃描其他批次。", | |||
| "No lot information for this item in the current order": "當前訂單中沒有此物品的批次資訊", | |||
| "Lot label print (pick station)": "批號標籤列印(提貨台)", | |||
| "Scan content": "掃碼內容", | |||
| "Scan then press Enter or click Look up": "掃描後按 Enter 或點「查詢」", | |||
| "Look up": "查詢", | |||
| "Clear": "清除", | |||
| "Printer": "印表機", | |||
| "Please select": "請選擇", | |||
| "Print copies": "列印張數", | |||
| "Refresh lot list": "刷新批號清單", | |||
| "Selected printer": "已選:{{printer}}", | |||
| "Item code name": "品號:{{code}} {{name}}", | |||
| "No available lots on this floor": "找不到該樓層有可用批號(availableQty > 0)。", | |||
| " (current lot)": "(當前批次)", | |||
| "Location with value": "位置:{{location}}", | |||
| "Available qty with uom": "可用量:{{qty}} 單位:{{uom}}", | |||
| "Print label": "列印標籤", | |||
| "Show QR": "顯示 QR", | |||
| "This row has no QR payload": "此列無法取得 QR payload(需 stockInLineId)", | |||
| "This pick line already scanned or completed, QR cannot be shown": "此出庫行已掃碼或已完成,無法顯示 QR", | |||
| "No lots available to print labels": "沒有任何批號可列印標籤", | |||
| "Failed to load printer list": "載入印表機清單失敗", | |||
| "Loaded available lots for this item": "已載入同品可用批號清單", | |||
| "Analysis failed": "分析失敗", | |||
| "Invalid itemId, cannot load lot list.": "無效 itemId,無法載入批號清單。", | |||
| "Invalid scan format. Please scan again.": "掃碼內容格式錯誤,請重新掃碼", | |||
| "Scan or look up once before refreshing the lot list.": "請先掃碼或查詢一次,才可刷新批號清單。", | |||
| "Print quantity must be an integer of 1 or more": "列印張數需為大於等於 1 的整數", | |||
| "Print sent: Lot {{lotNo}}": "已送出列印:Lot {{lotNo}}", | |||
| "Print failed": "列印失敗", | |||
| "Failed to load FG carton quantity. Please try again later.": "載入成品出倉出箱數量失敗,請稍後再試。", | |||
| "Cartons": "箱數", | |||
| "{{count}} cartons": "{{count}} 箱", | |||
| "No chart data": "沒有圖表資料", | |||
| "Total": "總數", | |||
| "2/F carton qty": "2/F 出箱數", | |||
| "4/F carton qty": "4/F 出箱數", | |||
| "Truck X carton qty": "車線-X 出箱數", | |||
| "Total carton qty": "總出箱數", | |||
| "Summary": "彙總", | |||
| "All floors": "全部樓層", | |||
| "Last 7 days": "最近7天", | |||
| "This month": "本月", | |||
| "This year": "本年", | |||
| "FG carton qty last 7 days title": "成品出倉出箱數量(最近7天)- {{floor}} - 基準日 {{date}}", | |||
| "FG carton qty this month title": "成品出倉出箱數量(本月)- {{floor}} - {{period}}", | |||
| "FG carton qty this year title": "成品出倉出箱數量(本年)- {{floor}} - {{period}}", | |||
| "Exporting...": "匯出中...", | |||
| "Download Excel": "下載 Excel", | |||
| "Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)", | |||
| "2/F or 4/F": "2/F 或 4/F", | |||
| "Lane": "車線", | |||
| "Generating...": "生成中...", | |||
| "Download report (PDF)": "下載報告 (PDF)", | |||
| "Unpicked orders confirm download": "此車線仍有 {{count}} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?", | |||
| "Failed to download Workbench truck routing summary. Please try again later.": "下載 Workbench 送貨路線摘要失敗,請稍後再試。", | |||
| "Confirm print drafts": "確認列印 {{count}} 張草稿?", | |||
| "Floor": "樓層", | |||
| "All": "全部" | |||
| } | |||
| @@ -150,44 +150,62 @@ export function buildUnpickableScanRowPatch( | |||
| return patch; | |||
| } | |||
| export function getWorkbenchSourceLotStatusSummary(lot: WorkbenchPickLotLike | null | undefined): { | |||
| export function getWorkbenchSourceLotStatusSummary( | |||
| lot: WorkbenchPickLotLike | null | undefined, | |||
| t?: PickOrderT, | |||
| ): { | |||
| severity: "success" | "warning" | "error"; | |||
| text: string; | |||
| } { | |||
| const tr = (key: string) => (t ? t(key) : key); | |||
| if (!lot) { | |||
| return { severity: "warning", text: "無法判斷此批號狀態" }; | |||
| return { severity: "warning", text: tr("Cannot determine this lot status") }; | |||
| } | |||
| if (isWorkbenchSourceLotExpired(lot)) { | |||
| return { severity: "error", text: "此批號狀態:已過期" }; | |||
| return { severity: "error", text: tr("Lot status: expired") }; | |||
| } | |||
| const solSt = solStatusOf(lot); | |||
| if (solSt === "rejected") { | |||
| return { severity: "warning", text: "此出庫行:已拒絕,請改掃其他批號" }; | |||
| return { | |||
| severity: "warning", | |||
| text: tr("This pick line was rejected. Please scan another lot."), | |||
| }; | |||
| } | |||
| if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") { | |||
| return { severity: "warning", text: "此出庫行:已完成,無需再提貨" }; | |||
| return { | |||
| severity: "warning", | |||
| text: tr("This pick line is already completed. No further pick needed."), | |||
| }; | |||
| } | |||
| const isNoLotRow = | |||
| lot.noLot === true || !lot.lotNo || String(lot.lotNo || "").trim() === ""; | |||
| if (isNoLotRow) { | |||
| return { | |||
| severity: "warning", | |||
| text: "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR", | |||
| text: tr( | |||
| "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: tr("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: tr("Lot status: depleted (available qty is 0)"), | |||
| }; | |||
| } | |||
| if (isInventoryLotLineUnavailable(lot)) { | |||
| return { | |||
| severity: "warning", | |||
| text: "此批號狀態:庫存不可用(未上架或行狀態不可用)", | |||
| text: tr("Lot status: unavailable (not put away or line unavailable)"), | |||
| }; | |||
| } | |||
| return { severity: "success", text: "此批號狀態:可提貨" }; | |||
| return { severity: "success", text: tr("Lot status: ready to pick") }; | |||
| } | |||