| @@ -186,7 +186,7 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| const confirmResult = await Swal.fire({ | const confirmResult = await Swal.fire({ | ||||
| title: t("Batch Print"), | title: t("Batch Print"), | ||||
| text: `${t("Confirm print: (")}${releasedOrders.length}${t("piece(s))")}`, | |||||
| text: t("Confirm print drafts", { count: releasedOrders.length }), | |||||
| icon: "question", | icon: "question", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonText: t("Confirm"), | confirmButtonText: t("Confirm"), | ||||
| @@ -276,18 +276,29 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| <Tabs | <Tabs | ||||
| value={tab} | value={tab} | ||||
| onChange={handleTabChange} | onChange={handleTabChange} | ||||
| variant="scrollable" | |||||
| scrollButtons="auto" | |||||
| allowScrollButtonsMobile | |||||
| sx={{ | sx={{ | ||||
| width: "100%", | |||||
| maxWidth: "100%", | |||||
| minHeight: 56, | |||||
| borderBottom: 1, | borderBottom: 1, | ||||
| borderColor: "divider", | borderColor: "divider", | ||||
| "& .MuiTabs-flexContainer": { | "& .MuiTabs-flexContainer": { | ||||
| columnGap: 2, | |||||
| rowGap: 1, | |||||
| columnGap: 0.5, | |||||
| }, | }, | ||||
| /* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */ | /* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */ | ||||
| "& .MuiTab-root": { | "& .MuiTab-root": { | ||||
| overflow: "visible", | 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={{ | sx={{ | ||||
| overflow: "visible", | overflow: "visible", | ||||
| /* 徽章在標籤右側外凸,預留空間避免與下一個 Tab 貼死 */ | /* 徽章在標籤右側外凸,預留空間避免與下一個 Tab 貼死 */ | ||||
| pr: etraIncompleteDopoCount > 99 ? 5 : etraIncompleteDopoCount > 0 ? 4 : 2, | |||||
| pr: etraIncompleteDopoCount > 99 ? 2.5 : etraIncompleteDopoCount > 0 ? 2 : 1, | |||||
| }} | }} | ||||
| label={ | label={ | ||||
| <Tooltip | <Tooltip | ||||
| @@ -329,7 +340,12 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| <Typography | <Typography | ||||
| component="span" | component="span" | ||||
| variant="inherit" | 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")} | {t("Etra Pick Order Detail")} | ||||
| </Typography> | </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")} value={2} /> | ||||
| <Tab label={t("Finished Good Record (All)")} value={3} /> | <Tab label={t("Finished Good Record (All)")} value={3} /> | ||||
| <Tab label={t("Ticket Release Table")} value={4} /> | <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> | </Tabs> | ||||
| <TabPanel value={tab} index={0}> | <TabPanel value={tab} index={0}> | ||||
| @@ -3,6 +3,7 @@ | |||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||
| import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material"; | import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material"; | ||||
| import DownloadIcon from "@mui/icons-material/Download"; | import DownloadIcon from "@mui/icons-material/Download"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { | import { | ||||
| @@ -18,6 +19,7 @@ import { | |||||
| } from "@/lib/featureUsageLog"; | } from "@/lib/featureUsageLog"; | ||||
| const TruckRoutingSummaryTabWorkbench: React.FC = () => { | const TruckRoutingSummaryTabWorkbench: React.FC = () => { | ||||
| const { t } = useTranslation(); | |||||
| const [storeOptions, setStoreOptions] = useState<WorkbenchReportOption[]>([]); | const [storeOptions, setStoreOptions] = useState<WorkbenchReportOption[]>([]); | ||||
| const [laneOptions, setLaneOptions] = useState<WorkbenchReportOption[]>([]); | const [laneOptions, setLaneOptions] = useState<WorkbenchReportOption[]>([]); | ||||
| const [storeId, setStoreId] = useState(""); | const [storeId, setStoreId] = useState(""); | ||||
| @@ -45,6 +47,9 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||||
| const canDownload = storeId && truckLanceCode && date && !loading; | const canDownload = storeId && truckLanceCode && date && !loading; | ||||
| const displayOptionLabel = (label: string) => | |||||
| String(label).trim() === "車線-X" ? t("Truck X") : label; | |||||
| const onDownload = async () => { | const onDownload = async () => { | ||||
| if (!canDownload) return; | if (!canDownload) return; | ||||
| try { | try { | ||||
| @@ -55,7 +60,9 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||||
| }); | }); | ||||
| if (precheck.hasUnpickedOrders) { | if (precheck.hasUnpickedOrders) { | ||||
| const confirmed = window.confirm( | const confirmed = window.confirm( | ||||
| `此車線仍有 ${precheck.unpickedOrderCount} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?` | |||||
| t("Unpicked orders confirm download", { | |||||
| count: precheck.unpickedOrderCount, | |||||
| }), | |||||
| ); | ); | ||||
| if (!confirmed) return; | if (!confirmed) return; | ||||
| } | } | ||||
| @@ -92,7 +99,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||||
| ); | ); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to download Workbench Truck Routing Summary", 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 { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| @@ -101,39 +108,39 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||||
| return ( | return ( | ||||
| <Box sx={{ maxWidth: 820 }}> | <Box sx={{ maxWidth: 820 }}> | ||||
| <Typography variant="h6" sx={{ mb: 2 }}> | <Typography variant="h6" sx={{ mb: 2 }}> | ||||
| 送貨路線摘要 (Workbench) | |||||
| {t("Truck Routing Summary (Workbench)")} | |||||
| </Typography> | </Typography> | ||||
| <Stack direction={{ xs: "column", md: "row" }} spacing={2} sx={{ mb: 2 }}> | <Stack direction={{ xs: "column", md: "row" }} spacing={2} sx={{ mb: 2 }}> | ||||
| <TextField | <TextField | ||||
| select | select | ||||
| fullWidth | fullWidth | ||||
| label="2/F 或 4/F" | |||||
| label={t("2/F or 4/F")} | |||||
| value={storeId} | value={storeId} | ||||
| onChange={(e) => onStoreChange(e.target.value)} | onChange={(e) => onStoreChange(e.target.value)} | ||||
| > | > | ||||
| {storeOptions.map((opt) => ( | {storeOptions.map((opt) => ( | ||||
| <MenuItem key={opt.value} value={opt.value}> | <MenuItem key={opt.value} value={opt.value}> | ||||
| {opt.label} | |||||
| {displayOptionLabel(opt.label)} | |||||
| </MenuItem> | </MenuItem> | ||||
| ))} | ))} | ||||
| </TextField> | </TextField> | ||||
| <TextField | <TextField | ||||
| select | select | ||||
| fullWidth | fullWidth | ||||
| label="車線" | |||||
| label={t("Lane")} | |||||
| value={truckLanceCode} | value={truckLanceCode} | ||||
| onChange={(e) => setTruckLanceCode(e.target.value)} | onChange={(e) => setTruckLanceCode(e.target.value)} | ||||
| disabled={!storeId} | disabled={!storeId} | ||||
| > | > | ||||
| {laneOptions.map((opt) => ( | {laneOptions.map((opt) => ( | ||||
| <MenuItem key={opt.value} value={opt.value}> | <MenuItem key={opt.value} value={opt.value}> | ||||
| {opt.label} | |||||
| {displayOptionLabel(opt.label)} | |||||
| </MenuItem> | </MenuItem> | ||||
| ))} | ))} | ||||
| </TextField> | </TextField> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label="日期" | |||||
| label={t("Date")} | |||||
| type="date" | type="date" | ||||
| value={date} | value={date} | ||||
| InputLabelProps={{ shrink: true }} | InputLabelProps={{ shrink: true }} | ||||
| @@ -146,7 +153,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||||
| disabled={!canDownload} | disabled={!canDownload} | ||||
| onClick={onDownload} | onClick={onDownload} | ||||
| > | > | ||||
| {loading ? "生成中..." : "下載報告 (PDF)"} | |||||
| {loading ? t("Generating...") : t("Download report (PDF)")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -421,22 +421,30 @@ function isWorkbenchSourceLotExpired(lot: any): boolean { | |||||
| return false; | 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"; | severity: "success" | "warning" | "error"; | ||||
| text: string; | text: string; | ||||
| } { | } { | ||||
| if (!lot) { | if (!lot) { | ||||
| return { severity: "warning", text: "無法判斷此批號狀態" }; | |||||
| return { severity: "warning", text: t("Cannot determine this lot status") }; | |||||
| } | } | ||||
| if (isWorkbenchSourceLotExpired(lot)) { | if (isWorkbenchSourceLotExpired(lot)) { | ||||
| return { severity: "error", text: "此批號狀態:已過期" }; | |||||
| return { severity: "error", text: t("Lot status: expired") }; | |||||
| } | } | ||||
| const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); | const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); | ||||
| if (solSt === "rejected") { | 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") { | 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,語意是「尚無可出庫批號」而非「已用畢」。 | * 無批次列:後端仍標 insufficient_stock,語意是「尚無可出庫批號」而非「已用畢」。 | ||||
| @@ -449,28 +457,28 @@ function getWorkbenchSourceLotStatusSummary(lot: any): { | |||||
| if (isNoLotRow) { | if (isNoLotRow) { | ||||
| return { | return { | ||||
| severity: "warning", | 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(); | const av = String(lot.lotAvailability || "").toLowerCase(); | ||||
| if (av === "insufficient_stock") { | if (av === "insufficient_stock") { | ||||
| return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" }; | |||||
| return { severity: "warning", text: t("Lot status: depleted (no remaining stock)") }; | |||||
| } | } | ||||
| const avail = Number(lot.availableQty); | const avail = Number(lot.availableQty); | ||||
| if (lot.lotNo && Number.isFinite(avail) && avail <= 0) { | 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)) { | if (isInventoryLotLineUnavailable(lot)) { | ||||
| return { | return { | ||||
| severity: "warning", | 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 { | function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string { | ||||
| const msg = raw.trim(); | const msg = raw.trim(); | ||||
| if (!msg) return msg; | if (!msg) return msg; | ||||
| @@ -1334,9 +1342,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| severity: undefined as "success" | "warning" | "error" | undefined, | severity: undefined as "success" | "warning" | "error" | undefined, | ||||
| }; | }; | ||||
| } | } | ||||
| const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot); | |||||
| const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot, t); | |||||
| return { text: s.text, severity: s.severity }; | return { text: s.text, severity: s.severity }; | ||||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot]); | |||||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, t]); | |||||
| const workbenchLotLabelSubmitQty = useMemo(() => { | const workbenchLotLabelSubmitQty = useMemo(() => { | ||||
| if (!workbenchLotLabelContextLot) return 0; | if (!workbenchLotLabelContextLot) return 0; | ||||
| @@ -1811,7 +1819,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| setQrScanErrorMsg( | 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 | // Mark this SOL as processed to prevent re-processing | ||||
| @@ -1864,7 +1874,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| startTransition(() => { | startTransition(() => { | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| setQrScanErrorMsg("当前订单中没有此物品的批次信息"); | |||||
| setQrScanErrorMsg(t("No lot information for this item in the current order")); | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -37,6 +37,7 @@ import { | |||||
| printWorkbenchLotLabel, | printWorkbenchLotLabel, | ||||
| } from "@/app/api/doworkbench/actions"; | } from "@/app/api/doworkbench/actions"; | ||||
| import { QRCodeSVG } from "qrcode.react"; | import { QRCodeSVG } from "qrcode.react"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| type ScanPayload = { | type ScanPayload = { | ||||
| itemId: number; | itemId: number; | ||||
| @@ -167,6 +168,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| submitQty = null, | submitQty = null, | ||||
| onSubmitQtyChange, | onSubmitQtyChange, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | |||||
| const scanInputRef = useRef<HTMLInputElement | null>(null); | const scanInputRef = useRef<HTMLInputElement | null>(null); | ||||
| const [scanInput, setScanInput] = useState(""); | const [scanInput, setScanInput] = useState(""); | ||||
| const [scanError, setScanError] = useState<string | null>(null); | const [scanError, setScanError] = useState<string | null>(null); | ||||
| @@ -210,8 +212,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!open) return; | if (!open) return; | ||||
| resetAll(); | resetAll(); | ||||
| const t = setTimeout(() => scanInputRef.current?.focus(), 50); | |||||
| return () => clearTimeout(t); | |||||
| const focusTimer = setTimeout(() => scanInputRef.current?.focus(), 50); | |||||
| return () => clearTimeout(focusTimer); | |||||
| }, [open, resetAll]); | }, [open, resetAll]); | ||||
| const loadPrinters = useCallback(async () => { | const loadPrinters = useCallback(async () => { | ||||
| @@ -224,13 +226,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| setPrinters([]); | setPrinters([]); | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: e instanceof Error ? e.message : "載入印表機清單失敗", | |||||
| message: e instanceof Error ? e.message : t("Failed to load printer list"), | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| } finally { | } finally { | ||||
| setPrintersLoading(false); | setPrintersLoading(false); | ||||
| } | } | ||||
| }, []); | |||||
| }, [t]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!open) return; | if (!open) return; | ||||
| @@ -283,23 +285,23 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| setAnalysis(data); | setAnalysis(data); | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: "已載入同品可用批號清單", | |||||
| message: t("Loaded available lots for this item"), | |||||
| severity: "success", | severity: "success", | ||||
| }); | }); | ||||
| } catch (e) { | } catch (e) { | ||||
| setAnalysis(null); | setAnalysis(null); | ||||
| setScanError(e instanceof Error ? e.message : "分析失敗"); | |||||
| setScanError(e instanceof Error ? e.message : t("Analysis failed")); | |||||
| } finally { | } finally { | ||||
| setAnalysisLoading(false); | setAnalysisLoading(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [resolveExpectedUomId], | |||||
| [resolveExpectedUomId, t], | |||||
| ); | ); | ||||
| const analyzeByItem = useCallback( | const analyzeByItem = useCallback( | ||||
| async (itemId: number) => { | async (itemId: number) => { | ||||
| if (!Number.isFinite(itemId) || itemId <= 0) { | if (!Number.isFinite(itemId) || itemId <= 0) { | ||||
| setScanError("無效 itemId,無法載入批號清單。"); | |||||
| setScanError(t("Invalid itemId, cannot load lot list.")); | |||||
| return; | return; | ||||
| } | } | ||||
| setLastItemId(itemId); | setLastItemId(itemId); | ||||
| @@ -325,17 +327,17 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| }); | }); | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: "已載入同品可用批號清單", | |||||
| message: t("Loaded available lots for this item"), | |||||
| severity: "success", | severity: "success", | ||||
| }); | }); | ||||
| } catch (e) { | } catch (e) { | ||||
| setAnalysis(null); | setAnalysis(null); | ||||
| setScanError(e instanceof Error ? e.message : "分析失敗"); | |||||
| setScanError(e instanceof Error ? e.message : t("Analysis failed")); | |||||
| } finally { | } finally { | ||||
| setAnalysisLoading(false); | setAnalysisLoading(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [resolveExpectedUomId], | |||||
| [resolveExpectedUomId, t], | |||||
| ); | ); | ||||
| const handleAnalyze = useCallback(async () => { | const handleAnalyze = useCallback(async () => { | ||||
| @@ -343,13 +345,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| const payload = safeParseScanPayload(raw); | const payload = safeParseScanPayload(raw); | ||||
| if (!payload) { | if (!payload) { | ||||
| setScanError( | setScanError( | ||||
| '掃碼內容格式錯誤,請重新掃碼', | |||||
| t("Invalid scan format. Please scan again."), | |||||
| ); | ); | ||||
| setAnalysis(null); | setAnalysis(null); | ||||
| return; | return; | ||||
| } | } | ||||
| await analyzePayload(payload); | await analyzePayload(payload); | ||||
| }, [scanInput, analyzePayload]); | |||||
| }, [scanInput, analyzePayload, t]); | |||||
| const handleRefreshLots = useCallback(async () => { | const handleRefreshLots = useCallback(async () => { | ||||
| const payload = lastPayload ?? safeParseScanPayload(scanInput.trim()); | const payload = lastPayload ?? safeParseScanPayload(scanInput.trim()); | ||||
| @@ -368,12 +370,12 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| if (!payload) { | if (!payload) { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: "請先掃碼或查詢一次,才可刷新批號清單。", | |||||
| message: t("Scan or look up once before refreshing the lot list."), | |||||
| severity: "info", | severity: "info", | ||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| }, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput]); | |||||
| }, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput, t]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!open) return; | if (!open) return; | ||||
| @@ -470,7 +472,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| if (selectedPrinterId === "") { | if (selectedPrinterId === "") { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: "請先選擇印表機", | |||||
| message: t("Please select a printer first"), | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| return; | return; | ||||
| @@ -478,7 +480,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| if (printQty < 1 || !Number.isFinite(printQty)) { | if (printQty < 1 || !Number.isFinite(printQty)) { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: "列印張數需為大於等於 1 的整數", | |||||
| message: t("Print quantity must be an integer of 1 or more"), | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| return; | return; | ||||
| @@ -493,25 +495,25 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| }); | }); | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: `已送出列印:Lot ${lotNo}`, | |||||
| message: t("Print sent: Lot {{lotNo}}", { lotNo }), | |||||
| severity: "success", | severity: "success", | ||||
| }); | }); | ||||
| } catch (e) { | } catch (e) { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: e instanceof Error ? e.message : "列印失敗", | |||||
| message: e instanceof Error ? e.message : t("Print failed"), | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| } finally { | } finally { | ||||
| setPrintingLotLineId(null); | setPrintingLotLineId(null); | ||||
| } | } | ||||
| }, | }, | ||||
| [selectedPrinterId, printQty], | |||||
| [selectedPrinterId, printQty, t], | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> | <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> | ||||
| <DialogTitle>批號標籤列印(提貨台)</DialogTitle> | |||||
| <DialogTitle>{t("Lot label print (pick station)")}</DialogTitle> | |||||
| <DialogContent> | <DialogContent> | ||||
| <Stack spacing={2} sx={{ mt: 1 }}> | <Stack spacing={2} sx={{ mt: 1 }}> | ||||
| {statusTitleText ? ( | {statusTitleText ? ( | ||||
| @@ -548,13 +550,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| > | > | ||||
| <TextField | <TextField | ||||
| inputRef={scanInputRef} | inputRef={scanInputRef} | ||||
| label="掃碼內容" | |||||
| label={t("Scan content")} | |||||
| value={scanInput} | value={scanInput} | ||||
| onChange={(e) => setScanInput(e.target.value)} | onChange={(e) => setScanInput(e.target.value)} | ||||
| fullWidth | fullWidth | ||||
| size="small" | size="small" | ||||
| error={!!scanError} | error={!!scanError} | ||||
| helperText={scanError || "掃描後按 Enter 或點「查詢」"} | |||||
| helperText={scanError || t("Scan then press Enter or click Look up")} | |||||
| onKeyDown={(e) => { | onKeyDown={(e) => { | ||||
| if (e.key === "Enter") { | if (e.key === "Enter") { | ||||
| e.preventDefault(); | e.preventDefault(); | ||||
| @@ -568,7 +570,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| onClick={() => void handleAnalyze()} | onClick={() => void handleAnalyze()} | ||||
| disabled={analysisLoading || !scanInput.trim()} | disabled={analysisLoading || !scanInput.trim()} | ||||
| > | > | ||||
| {analysisLoading ? <CircularProgress size={18} /> : "查詢"} | |||||
| {analysisLoading ? <CircularProgress size={18} /> : t("Look up")} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -578,7 +580,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| }} | }} | ||||
| disabled={analysisLoading} | disabled={analysisLoading} | ||||
| > | > | ||||
| 清除 | |||||
| {t("Clear")} | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| </> | </> | ||||
| @@ -594,16 +596,16 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| sx={{ minWidth: 260 }} | sx={{ minWidth: 260 }} | ||||
| disabled={printersLoading} | disabled={printersLoading} | ||||
| > | > | ||||
| <InputLabel>印表機</InputLabel> | |||||
| <InputLabel>{t("Printer")}</InputLabel> | |||||
| <Select | <Select | ||||
| label="印表機" | |||||
| label={t("Printer")} | |||||
| value={selectedPrinterId} | value={selectedPrinterId} | ||||
| onChange={(e) => | onChange={(e) => | ||||
| setSelectedPrinterId((e.target.value as number) ?? "") | setSelectedPrinterId((e.target.value as number) ?? "") | ||||
| } | } | ||||
| > | > | ||||
| <MenuItem value=""> | <MenuItem value=""> | ||||
| <em>{printersLoading ? "載入中..." : "請選擇"}</em> | |||||
| <em>{printersLoading ? t("Loading") : t("Please select")}</em> | |||||
| </MenuItem> | </MenuItem> | ||||
| {printers.map((p) => ( | {printers.map((p) => ( | ||||
| <MenuItem key={p.id} value={p.id}> | <MenuItem key={p.id} value={p.id}> | ||||
| @@ -614,7 +616,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| </FormControl> | </FormControl> | ||||
| <TextField | <TextField | ||||
| label="列印張數" | |||||
| label={t("Print copies")} | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| inputProps={{ min: 1, step: 1 }} | inputProps={{ min: 1, step: 1 }} | ||||
| @@ -626,7 +628,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| {onWorkbenchScanPick ? ( | {onWorkbenchScanPick ? ( | ||||
| <TextField | <TextField | ||||
| label="提交數量" | |||||
| label={t("Submit Qty")} | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| inputProps={{ min: 0, step: 1 }} | inputProps={{ min: 0, step: 1 }} | ||||
| @@ -651,7 +653,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| {analysisLoading ? ( | {analysisLoading ? ( | ||||
| <CircularProgress size={18} /> | <CircularProgress size={18} /> | ||||
| ) : ( | ) : ( | ||||
| "刷新批號清單" | |||||
| t("Refresh lot list") | |||||
| )} | )} | ||||
| </Button> | </Button> | ||||
| @@ -661,7 +663,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| color="text.secondary" | color="text.secondary" | ||||
| sx={{ ml: { md: "auto" } }} | sx={{ ml: { md: "auto" } }} | ||||
| > | > | ||||
| 已選:{formatPrinterLabel(selectedPrinter)} | |||||
| {t("Selected printer", { printer: formatPrinterLabel(selectedPrinter) })} | |||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| @@ -669,12 +671,12 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| {analysis && ( | {analysis && ( | ||||
| <Box> | <Box> | ||||
| <Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}> | <Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}> | ||||
| 品號:{analysis.itemCode} {analysis.itemName} | |||||
| {t("Item code name", { code: analysis.itemCode, name: analysis.itemName })} | |||||
| </Typography> | </Typography> | ||||
| {filteredLots.length === 0 ? ( | {filteredLots.length === 0 ? ( | ||||
| <Alert severity="warning"> | <Alert severity="warning"> | ||||
| 找不到該樓層有可用批號(availableQty > 0)。 | |||||
| {t("No available lots on this floor")} | |||||
| </Alert> | </Alert> | ||||
| ) : ( | ) : ( | ||||
| <Stack spacing={1}> | <Stack spacing={1}> | ||||
| @@ -717,14 +719,16 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| sx={{ fontWeight: lot._scanned ? 800 : 600 }} | sx={{ fontWeight: lot._scanned ? 800 : 600 }} | ||||
| > | > | ||||
| Lot:{lot.lotNo} | Lot:{lot.lotNo} | ||||
| {lot._scanned ? "(當前批次)" : ""} | |||||
| {lot._scanned ? t(" (current lot)") : ""} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| 位置:{loc || "—"} | |||||
| {t("Location with value", { location: loc || "—" })} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <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> | </Typography> | ||||
| </Box> | </Box> | ||||
| <Stack | <Stack | ||||
| @@ -747,7 +751,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| {isPrinting ? ( | {isPrinting ? ( | ||||
| <CircularProgress size={18} /> | <CircularProgress size={18} /> | ||||
| ) : ( | ) : ( | ||||
| "列印標籤" | |||||
| t("Print label") | |||||
| )} | )} | ||||
| </Button> | </Button> | ||||
| {onWorkbenchScanPick ? ( | {onWorkbenchScanPick ? ( | ||||
| @@ -756,9 +760,9 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| color="secondary" | color="secondary" | ||||
| title={ | title={ | ||||
| !lotQrPayload | !lotQrPayload | ||||
| ? "此列無法取得 QR payload(需 stockInLineId)" | |||||
| ? t("This row has no QR payload") | |||||
| : disableScanPick | : disableScanPick | ||||
| ? "此出庫行已掃碼或已完成,無法顯示 QR" | |||||
| ? t("This pick line already scanned or completed, QR cannot be shown") | |||||
| : undefined | : undefined | ||||
| } | } | ||||
| disabled={ | disabled={ | ||||
| @@ -772,7 +776,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| ) | ) | ||||
| } | } | ||||
| > | > | ||||
| 顯示 QR | |||||
| {t("Show QR")} | |||||
| </Button> | </Button> | ||||
| ) : null} | ) : null} | ||||
| </Stack> | </Stack> | ||||
| @@ -809,14 +813,14 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {onWorkbenchScanPick | {onWorkbenchScanPick | ||||
| ? "沒有任何批號可列印標籤" | |||||
| ? t("No lots available to print labels") | |||||
| : ""} | : ""} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| <Button onClick={onClose}>關閉</Button> | |||||
| <Button onClick={onClose}>{t("Close")}</Button> | |||||
| </DialogActions> | </DialogActions> | ||||
| <Snackbar | <Snackbar | ||||
| @@ -28,6 +28,7 @@ import { | |||||
| fetchCompletedDoPickOrdersWorkbenchAll, | fetchCompletedDoPickOrdersWorkbenchAll, | ||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| import SafeApexCharts from "@/components/charts/SafeApexCharts"; | import SafeApexCharts from "@/components/charts/SafeApexCharts"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| type FloorFilter = "all" | "2/F" | "4/F"; | type FloorFilter = "all" | "2/F" | "4/F"; | ||||
| @@ -44,6 +45,8 @@ type Props = { | |||||
| }; | }; | ||||
| const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => { | 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 [floor, setFloor] = useState<FloorFilter>("all"); | ||||
| const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD")); | const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD")); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| @@ -66,12 +69,12 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| setRecords(data); | setRecords(data); | ||||
| } catch (err) { | } catch (err) { | ||||
| console.error("Failed to load finished good carton dashboard data", 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([]); | setRecords([]); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, [date, mode]); | |||||
| }, [date, mode, t]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| loadData(); | loadData(); | ||||
| @@ -132,37 +135,40 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| }, | }, | ||||
| xaxis: { | xaxis: { | ||||
| categories: rows.map((row) => row.date), | categories: rows.map((row) => row.date), | ||||
| title: { text: "日期" }, | |||||
| title: { text: t("Date") }, | |||||
| }, | }, | ||||
| yaxis: { | yaxis: { | ||||
| title: { text: "箱數" }, | |||||
| title: { text: t("Cartons") }, | |||||
| labels: { | labels: { | ||||
| formatter: (val) => Number(val || 0).toLocaleString("zh-HK"), | |||||
| formatter: (val) => Number(val || 0).toLocaleString(numberLocale), | |||||
| }, | }, | ||||
| }, | }, | ||||
| tooltip: { | tooltip: { | ||||
| y: { | y: { | ||||
| formatter: (val) => `${Number(val || 0).toLocaleString("zh-HK")} 箱`, | |||||
| formatter: (val) => | |||||
| t("{{count}} cartons", { | |||||
| count: Number(val || 0).toLocaleString(numberLocale), | |||||
| }), | |||||
| }, | }, | ||||
| }, | }, | ||||
| legend: { | legend: { | ||||
| position: "top", | position: "top", | ||||
| }, | }, | ||||
| noData: { | noData: { | ||||
| text: "沒有圖表資料", | |||||
| text: t("No chart data"), | |||||
| }, | }, | ||||
| }), | }), | ||||
| [rows], | |||||
| [rows, t, numberLocale], | |||||
| ); | ); | ||||
| const chartSeries = useMemo( | const chartSeries = useMemo( | ||||
| () => [ | () => [ | ||||
| { name: "2/F", data: rows.map((row) => row.floor2F) }, | { name: "2/F", data: rows.map((row) => row.floor2F) }, | ||||
| { name: "4/F", data: rows.map((row) => row.floor4F) }, | { 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(() => { | const summary = useMemo(() => { | ||||
| @@ -317,21 +323,27 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| const aoa: (string | number)[][] = [ | const aoa: (string | number)[][] = [ | ||||
| [reportTitle, "", "", "", ""], | [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]), | ...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); | const worksheet = XLSX.utils.aoa_to_sheet(aoa); | ||||
| styleWorksheet(worksheet, dailyRows.length); | styleWorksheet(worksheet, dailyRows.length); | ||||
| XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); | XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); | ||||
| }, | }, | ||||
| [calcSummary, styleWorksheet], | |||||
| [calcSummary, styleWorksheet, t], | |||||
| ); | ); | ||||
| const handleDownloadExcel = useCallback(async () => { | const handleDownloadExcel = useCallback(async () => { | ||||
| @@ -343,8 +355,14 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| : await fetchCompletedDoPickOrdersAll(); | : await fetchCompletedDoPickOrdersAll(); | ||||
| const baseDate = dayjs(date || undefined).isValid() ? dayjs(date) : dayjs(); | 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 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( | const last7Rows = buildDailyRowsFromRecords( | ||||
| allRecords, | allRecords, | ||||
| @@ -368,39 +386,42 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| const workbook = XLSX.utils.book_new(); | const workbook = XLSX.utils.book_new(); | ||||
| addReportSheet( | addReportSheet( | ||||
| workbook, | workbook, | ||||
| "最近7天", | |||||
| `成品出倉出箱數量(最近7天)- ${floorLabel} - 基準日 ${dateLabel}`, | |||||
| t("Last 7 days"), | |||||
| t("FG carton qty last 7 days title", { floor: floorLabel, date: dateLabel }), | |||||
| last7Rows, | last7Rows, | ||||
| ); | ); | ||||
| addReportSheet( | addReportSheet( | ||||
| workbook, | workbook, | ||||
| "本月", | |||||
| `成品出倉出箱數量(本月)- ${floorLabel} - ${baseDate.format("YYYY年MM月")}`, | |||||
| t("This month"), | |||||
| t("FG carton qty this month title", { floor: floorLabel, period: monthPeriod }), | |||||
| monthRows, | monthRows, | ||||
| ); | ); | ||||
| addReportSheet( | addReportSheet( | ||||
| workbook, | workbook, | ||||
| "本年", | |||||
| `成品出倉出箱數量(本年)- ${floorLabel} - ${baseDate.format("YYYY年")}`, | |||||
| t("This year"), | |||||
| t("FG carton qty this year title", { floor: floorLabel, period: yearPeriod }), | |||||
| yearRows, | yearRows, | ||||
| ); | ); | ||||
| XLSX.writeFile(workbook, `成品出倉出箱數量_多時段報表_${floorLabel.replace("/", "")}_${dateLabel}.xlsx`); | |||||
| XLSX.writeFile( | |||||
| workbook, | |||||
| `FG_carton_qty_${floorLabel.replace("/", "")}_${dateLabel}.xlsx`, | |||||
| ); | |||||
| } finally { | } finally { | ||||
| setIsExporting(false); | setIsExporting(false); | ||||
| } | } | ||||
| }, [mode, date, floor, buildDailyRowsFromRecords, addReportSheet]); | |||||
| }, [mode, date, floor, buildDailyRowsFromRecords, addReportSheet, t, i18n.language]); | |||||
| return ( | return ( | ||||
| <Box sx={{ width: "100%" }}> | <Box sx={{ width: "100%" }}> | ||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}> | <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}> | ||||
| <Typography variant="h6">成品出倉出箱數量</Typography> | |||||
| <Typography variant="h6">{t("FG Carton Qty")}</Typography> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={handleDownloadExcel} | onClick={handleDownloadExcel} | ||||
| disabled={loading || isExporting} | disabled={loading || isExporting} | ||||
| > | > | ||||
| {isExporting ? "匯出中..." : "下載 Excel"} | |||||
| {isExporting ? t("Exporting...") : t("Download Excel")} | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| @@ -421,11 +442,11 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| <TextField | <TextField | ||||
| select | select | ||||
| fullWidth | fullWidth | ||||
| label="樓層" | |||||
| label={t("Floor")} | |||||
| value={floor} | value={floor} | ||||
| onChange={(event) => setFloor(event.target.value as FloorFilter)} | 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="2/F">2/F</MenuItem> | ||||
| <MenuItem value="4/F">4/F</MenuItem> | <MenuItem value="4/F">4/F</MenuItem> | ||||
| </TextField> | </TextField> | ||||
| @@ -433,7 +454,7 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| <Grid item xs={12} md={6}> | <Grid item xs={12} md={6}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label="日期" | |||||
| label={t("Date")} | |||||
| type="date" | type="date" | ||||
| value={date} | value={date} | ||||
| InputLabelProps={{ shrink: true }} | InputLabelProps={{ shrink: true }} | ||||
| @@ -448,20 +469,20 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||||
| <Table size="small"> | <Table size="small"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <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> | ||||
| <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> | ||||
| <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> | ||||
| <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> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -654,11 +654,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| const reminder = workbenchLotLabelReminderText?.trim() ?? ""; | const reminder = workbenchLotLabelReminderText?.trim() ?? ""; | ||||
| if (reminder && isExpiredWorkbenchReminderMessage(reminder)) { | 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 }; | return { text: s.text, severity: s.severity }; | ||||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, workbenchLotLabelReminderText]); | |||||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, workbenchLotLabelReminderText, t]); | |||||
| const handleJustComplete = useCallback( | const handleJustComplete = useCallback( | ||||
| async (row: LotRow) => { | async (row: LotRow) => { | ||||
| @@ -1714,7 +1714,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| onClick={() => openWorkbenchLotLabelModalForLot(r)} | onClick={() => openWorkbenchLotLabelModalForLot(r)} | ||||
| sx={{ flexShrink: 0, fontSize: "0.7rem", py: 0.25, minWidth: "auto", px: 1, whiteSpace: "nowrap" }} | sx={{ flexShrink: 0, fontSize: "0.7rem", py: 0.25, minWidth: "auto", px: 1, whiteSpace: "nowrap" }} | ||||
| > | > | ||||
| {t(" 批號 QR 碼")} | |||||
| {t("lot QR code")} | |||||
| </Button> | </Button> | ||||
| ) : null} | ) : null} | ||||
| </Stack> | </Stack> | ||||
| @@ -1,131 +1,131 @@ | |||||
| { | { | ||||
| "Actions": "操作", | |||||
| "Add Document": "新增文件", | |||||
| "Actions": "Actions", | |||||
| "Add Document": "Add Document", | |||||
| "All": "All", | "All": "All", | ||||
| "Allergic Substances": "過敏原", | |||||
| "Allergic Substances": "Allergens", | |||||
| "An error has occurred. Please try again later.": "An error has occurred. Please try again later.", | "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 Required Qty": "BOM Required Qty", | ||||
| "Bom UOM": "BOM UOM", | "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?", | "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", | "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", | "IP": "IP", | ||||
| "Item Code": "Item Code", | "Item Code": "Item Code", | ||||
| "Item Name": "Item Name", | "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", | "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", | "Pending": "Pending", | ||||
| "Port": "Port", | "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", | "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": "Search", | ||||
| "Search Criteria": "Search Criteria", | "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", | "Sign out": "Sign out", | ||||
| "Language": "Language", | "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", | "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", | "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", | "DO Workbench Search": "DO Workbench Search", | ||||
| "completed": "completed", | "completed": "completed", | ||||
| "items": "items", | "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", | "Lines": "Lines", | ||||
| "Before Today": "Before Today", | "Before Today": "Before Today", | ||||
| "Truck X": "Truck X", | "Truck X": "Truck X", | ||||
| "Finsihed good items": "Finsihed good items", | |||||
| "Finsihed good items": "Finished good items", | |||||
| "kinds": "kinds", | "kinds": "kinds", | ||||
| "Completed Date": "Completed Date", | "Completed Date": "Completed Date", | ||||
| "Completed Time": "Completed Time", | "Completed Time": "Completed Time", | ||||
| @@ -147,8 +147,8 @@ | |||||
| "Etra": "Etra", | "Etra": "Etra", | ||||
| "Exit Etra view": "Exit Etra view", | "Exit Etra view": "Exit Etra view", | ||||
| "Etra Pick Order Detail": "Etra Pick Order Detail", | "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", | "Back to normal assign tab": "Back to normal assign tab", | ||||
| "Enter isExtra workbench view?": "Enter isExtra workbench view?", | "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.", | "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", | "Confirm Search": "Search", | ||||
| "Merge Etra ticket search prompt": "Enter shop (optional) and date, then click Search to load merge candidates.", | "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.", | "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", | "Pick Order": "Pick Order", | ||||
| "Type": "Type", | "Type": "Type", | ||||
| "Product Type": "Product Type", | "Product Type": "Product Type", | ||||
| @@ -529,14 +528,14 @@ | |||||
| "Floor ticket": "Floor ticket", | "Floor ticket": "Floor ticket", | ||||
| "2F ticket": "2F ticket", | "2F ticket": "2F ticket", | ||||
| "4F ticket": "4F 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", | "label Printer": "label Printer", | ||||
| "Loading Sequence": "Loading Sequence", | "Loading Sequence": "Loading Sequence", | ||||
| "Ticket No": "Ticket No", | "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.", | "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 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.", | "Lot confirmation failed. Please try again.": "Lot confirmation failed. Please try again.", | ||||
| "Powder Mixture": "Powder Mixture", | "Powder Mixture": "Powder Mixture", | ||||
| @@ -559,5 +558,80 @@ | |||||
| "passed": "Passed", | "passed": "Passed", | ||||
| "failed": "Failed", | "failed": "Failed", | ||||
| "confirm_accept_with_fail": "There are failed QC items. Confirm to accept stock out?", | "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", | "Departure Time": "Departure Time", | ||||
| "Floor": "Floor", | "Floor": "Floor", | ||||
| "Force complete DO": "Force complete DO", | "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", | "Handler Name": "Handler Name", | ||||
| "Last updated": "Last updated", | "Last updated": "Last updated", | ||||
| "Loading Sequence": "Loading Sequence", | "Loading Sequence": "Loading Sequence", | ||||
| "Manager only hint": "Manager only hint", | |||||
| "Manager only hint": "Admin only", | |||||
| "No data available": "No data available", | "No data available": "No data available", | ||||
| "Now": "Now", | "Now": "Now", | ||||
| "Number of FG Items (Order Item(s) Count)": "Number of FG Items (Order Item(s) Count)", | "Number of FG Items (Order Item(s) Count)": "Number of FG Items (Order Item(s) Count)", | ||||
| @@ -29,7 +29,7 @@ | |||||
| "Reload data": "Reload data", | "Reload data": "Reload data", | ||||
| "Required Delivery Date": "Required Delivery Date", | "Required Delivery Date": "Required Delivery Date", | ||||
| "Revert assignment": "Revert assignment", | "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", | "Rows per page": "Rows per page", | ||||
| "Select All": "Select All", | "Select All": "Select All", | ||||
| "Select Date": "Select Date", | "Select Date": "Select Date", | ||||
| @@ -45,5 +45,19 @@ | |||||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | ||||
| "Last updated": "最後更新", | "Last updated": "最後更新", | ||||
| "Truck Information": "車線資訊", | "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.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。", | "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。", | ||||
| "No lot rows. Select a line in the table above.": "尚無批號資料。請在上方表格勾選一行提料單明細。", | "No lot rows. Select a line in the table above.": "尚無批號資料。請在上方表格勾選一行提料單明細。", | ||||
| "No stock out line for this lot": "此批號尚無出庫行,無法提交。", | "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; | return patch; | ||||
| } | } | ||||
| export function getWorkbenchSourceLotStatusSummary(lot: WorkbenchPickLotLike | null | undefined): { | |||||
| export function getWorkbenchSourceLotStatusSummary( | |||||
| lot: WorkbenchPickLotLike | null | undefined, | |||||
| t?: PickOrderT, | |||||
| ): { | |||||
| severity: "success" | "warning" | "error"; | severity: "success" | "warning" | "error"; | ||||
| text: string; | text: string; | ||||
| } { | } { | ||||
| const tr = (key: string) => (t ? t(key) : key); | |||||
| if (!lot) { | if (!lot) { | ||||
| return { severity: "warning", text: "無法判斷此批號狀態" }; | |||||
| return { severity: "warning", text: tr("Cannot determine this lot status") }; | |||||
| } | } | ||||
| if (isWorkbenchSourceLotExpired(lot)) { | if (isWorkbenchSourceLotExpired(lot)) { | ||||
| return { severity: "error", text: "此批號狀態:已過期" }; | |||||
| return { severity: "error", text: tr("Lot status: expired") }; | |||||
| } | } | ||||
| const solSt = solStatusOf(lot); | const solSt = solStatusOf(lot); | ||||
| if (solSt === "rejected") { | 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") { | 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 = | const isNoLotRow = | ||||
| lot.noLot === true || !lot.lotNo || String(lot.lotNo || "").trim() === ""; | lot.noLot === true || !lot.lotNo || String(lot.lotNo || "").trim() === ""; | ||||
| if (isNoLotRow) { | if (isNoLotRow) { | ||||
| return { | return { | ||||
| severity: "warning", | 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(); | const av = String(lot.lotAvailability || "").toLowerCase(); | ||||
| if (av === "insufficient_stock") { | if (av === "insufficient_stock") { | ||||
| return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" }; | |||||
| return { | |||||
| severity: "warning", | |||||
| text: tr("Lot status: depleted (no remaining stock)"), | |||||
| }; | |||||
| } | } | ||||
| const avail = Number(lot.availableQty); | const avail = Number(lot.availableQty); | ||||
| if (lot.lotNo && Number.isFinite(avail) && avail <= 0) { | 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)) { | if (isInventoryLotLineUnavailable(lot)) { | ||||
| return { | return { | ||||
| severity: "warning", | 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") }; | |||||
| } | } | ||||