| Autor | SHA1 | Mensagem | Data |
|---|---|---|---|
|
|
759df0b379 | [Fix] Amend few visual issues | há 1 semana |
|
|
4665592671 | [Feat] DO Workbench -> Carton Qty now can search lane and shop + download btn for specified filters | há 1 semana |
|
|
163b634c7e | [Fix] DO Workbench EN language | há 1 semana |
|
|
a6379b291f | Merge branch 'production' of http://svn.2fi-solutions.com:8300/derek/FPSMS-frontend into do_workbench_fix | há 1 semana |
|
|
26ab7cc117 | allow mutli sync for do in m18 sync ui | há 1 semana |
| @@ -135,26 +135,55 @@ export default function M18SynPage() { | |||||
| } | } | ||||
| }; | }; | ||||
| /** 送貨訂單:手動按 code 同步(可輸入多個 code,用逗號或換行分隔) */ | |||||
| const handleSyncM18DoByCode = async () => { | const handleSyncM18DoByCode = async () => { | ||||
| if (m18DoInFlightRef.current) return; | if (m18DoInFlightRef.current) return; | ||||
| if (!m18DoCode.trim()) { | |||||
| alert("Please enter DO / shop PO code."); | |||||
| const raw = m18DoCode.trim(); | |||||
| if (!raw) { | |||||
| alert("Please enter DO / shop PO code(s)."); | |||||
| return; | |||||
| } | |||||
| const codes = raw | |||||
| .split(/[\n,]+/g) | |||||
| .map((s) => s.trim()) | |||||
| .filter(Boolean); | |||||
| if (codes.length === 0) { | |||||
| alert("Please enter at least one code."); | |||||
| return; | return; | ||||
| } | } | ||||
| m18DoInFlightRef.current = true; | m18DoInFlightRef.current = true; | ||||
| setIsSyncingM18Do(true); | setIsSyncingM18Do(true); | ||||
| setM18DoSyncResult(""); | setM18DoSyncResult(""); | ||||
| try { | try { | ||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/m18/test/do-by-code?code=${encodeURIComponent(m18DoCode.trim())}`, | |||||
| { method: "GET" }, | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| const text = await response.text(); | |||||
| setM18DoSyncResult(formatSyncResultText("送貨訂單", text)); | |||||
| if (!response.ok) { | |||||
| alert(`Sync failed: ${response.status}`); | |||||
| const outputs: string[] = []; | |||||
| const okCodes: string[] = []; | |||||
| const notFoundCodes: string[] = []; | |||||
| const otherFailCodes: string[] = []; | |||||
| for (const code of codes) { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/m18/test/do-by-code?code=${encodeURIComponent(code)}`, | |||||
| { method: "GET" }, | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| const text = await response.text(); | |||||
| const parsed = parseSyncResult("送貨訂單", text || ""); | |||||
| if (parsed.kind === "success") okCodes.push(code); | |||||
| else if (parsed.kind === "not_found") notFoundCodes.push(code); | |||||
| else otherFailCodes.push(code); | |||||
| const perCodeMsg = | |||||
| parsed.kind === "success" ? "成功同步" : | |||||
| parsed.kind === "not_found" ? parsed.message : | |||||
| parsed.message; | |||||
| outputs.push(`${code}: ${perCodeMsg}${response.ok ? "" : ` (HTTP ${response.status})`}`); | |||||
| } | } | ||||
| outputs.push(""); | |||||
| outputs.push(`共${okCodes.length}張送貨訂單成功`); | |||||
| if (notFoundCodes.length > 0) outputs.push(`共${notFoundCodes.length}張在M18找不到訂單 (${notFoundCodes.join(", ")})`); | |||||
| if (otherFailCodes.length > 0) outputs.push(`共${otherFailCodes.length}張同步失敗 (${otherFailCodes.join(", ")})`); | |||||
| setM18DoSyncResult(outputs.join("\n")); | |||||
| } catch (e) { | } catch (e) { | ||||
| console.error("M18 DO Sync By Code Error:", e); | console.error("M18 DO Sync By Code Error:", e); | ||||
| alert("M18 DO sync failed. Check console/network."); | alert("M18 DO sync failed. Check console/network."); | ||||
| @@ -312,18 +341,22 @@ export default function M18SynPage() { | |||||
| <TabPanel value={tabValue} index={1}> | <TabPanel value={tabValue} index={1}> | ||||
| <Section title="M18 送貨訂單 — sync by code"> | <Section title="M18 送貨訂單 — sync by code"> | ||||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}> | |||||
| <Stack spacing={2} sx={{ mb: 2 }}> | |||||
| <TextField | <TextField | ||||
| size="small" | |||||
| label="DO / Shop PO Code" | label="DO / Shop PO Code" | ||||
| value={m18DoCode} | value={m18DoCode} | ||||
| onChange={(e) => setM18DoCode(e.target.value)} | onChange={(e) => setM18DoCode(e.target.value)} | ||||
| placeholder="e.g. same document code as M18 shop PO" | |||||
| sx={{ minWidth: 320 }} | |||||
| placeholder="可輸入多個 code,用逗號或換行分隔" | |||||
| fullWidth | |||||
| multiline | |||||
| minRows={6} | |||||
| maxRows={14} | |||||
| /> | /> | ||||
| <Button variant="contained" color="primary" onClick={handleSyncM18DoByCode} disabled={isSyncingM18Do}> | |||||
| {isSyncingM18Do ? "Syncing..." : "Sync DO from M18"} | |||||
| </Button> | |||||
| <Box sx={{ display: "flex", justifyContent: "flex-end" }}> | |||||
| <Button variant="contained" color="primary" onClick={handleSyncM18DoByCode} disabled={isSyncingM18Do}> | |||||
| {isSyncingM18Do ? "Syncing..." : "Sync DO from M18"} | |||||
| </Button> | |||||
| </Box> | |||||
| </Stack> | </Stack> | ||||
| {m18DoSyncResult ? ( | {m18DoSyncResult ? ( | ||||
| <TextField | <TextField | ||||
| @@ -216,7 +216,6 @@ export async function fetchWorkbenchStoreLaneSummary( | |||||
| return serverFetchJson<StoreLaneSummary>(url, { | return serverFetchJson<StoreLaneSummary>(url, { | ||||
| method: "GET", | method: "GET", | ||||
| cache: "no-store", | cache: "no-store", | ||||
| next: { revalidate: 0 }, | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -235,7 +234,6 @@ export async function fetchWorkbenchEtraLaneSummary( | |||||
| const data = await serverFetchJson<WorkbenchEtraShopLaneGroup[]>(url, { | const data = await serverFetchJson<WorkbenchEtraShopLaneGroup[]>(url, { | ||||
| method: "GET", | method: "GET", | ||||
| cache: "no-store", | cache: "no-store", | ||||
| next: { revalidate: 0 }, | |||||
| }); | }); | ||||
| return Array.isArray(data) ? data : []; | return Array.isArray(data) ? data : []; | ||||
| } | } | ||||
| @@ -632,7 +632,6 @@ export async function fetchStoreLaneSummary(storeId: string, requiredDate?: stri | |||||
| const response = await serverFetchJson<StoreLaneSummary>(url, { | const response = await serverFetchJson<StoreLaneSummary>(url, { | ||||
| method: "GET", | method: "GET", | ||||
| cache: "no-store", | cache: "no-store", | ||||
| next: { revalidate: 0 }, | |||||
| }); | }); | ||||
| console.timeEnd(label); | console.timeEnd(label); | ||||
| return response; | return response; | ||||
| @@ -856,7 +855,6 @@ export const fetchFGPickOrdersByUserIdWorkbench = async (userId: number) => { | |||||
| method: "GET", | method: "GET", | ||||
| // Must be fresh: determines whether shell shows Floor/Lane panel or Detail. | // Must be fresh: determines whether shell shows Floor/Lane panel or Detail. | ||||
| cache: "no-store", | cache: "no-store", | ||||
| next: { revalidate: 0 }, | |||||
| }, | }, | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -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: 48, | |||||
| 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: 48, | |||||
| minWidth: 72, | |||||
| maxWidth: "none", | |||||
| px: 1.5, | |||||
| py: 0.75, | |||||
| whiteSpace: "nowrap", | |||||
| 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: "nowrap", | |||||
| 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 | ||||
| @@ -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> | ||||
| @@ -150,6 +150,34 @@ function buildApexConfig( | |||||
| const EMPTY_MESSAGE = "暫無圖表資料(後端無法連線或此區間無資料)。"; | const EMPTY_MESSAGE = "暫無圖表資料(後端無法連線或此區間無資料)。"; | ||||
| /** Apex mutates `options` in place (circular refs). Never throw during render. */ | |||||
| function safeStringify(value: unknown): string { | |||||
| const seen = new WeakSet<object>(); | |||||
| try { | |||||
| return JSON.stringify(value, (_key, nested) => { | |||||
| if (typeof nested === "function") return undefined; | |||||
| if (typeof nested === "object" && nested !== null) { | |||||
| if (seen.has(nested)) return undefined; | |||||
| seen.add(nested); | |||||
| } | |||||
| return nested; | |||||
| }); | |||||
| } catch { | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| function destroyChart(chartRef: { current: { destroy: () => void } | null }) { | |||||
| const current = chartRef.current; | |||||
| chartRef.current = null; | |||||
| if (!current) return; | |||||
| try { | |||||
| current.destroy(); | |||||
| } catch { | |||||
| /* ignore */ | |||||
| } | |||||
| } | |||||
| export default function SafeApexCharts(props: SafeApexChartsProps) { | export default function SafeApexCharts(props: SafeApexChartsProps) { | ||||
| const { type, series, options, height, width, chartRevision, allowZeroSeries = false, ...rest } = props; | const { type, series, options, height, width, chartRevision, allowZeroSeries = false, ...rest } = props; | ||||
| const containerRef = useRef<HTMLDivElement>(null); | const containerRef = useRef<HTMLDivElement>(null); | ||||
| @@ -162,18 +190,11 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||||
| sanitized = alignSeriesToCategoryCount(sanitized, cats.length); | sanitized = alignSeriesToCategoryCount(sanitized, cats.length); | ||||
| } | } | ||||
| if (shouldShowPlaceholder(type, options, sanitized, allowZeroSeries)) { | |||||
| return ( | |||||
| <Typography color="text.secondary" sx={{ py: 3 }}> | |||||
| {EMPTY_MESSAGE} | |||||
| </Typography> | |||||
| ); | |||||
| } | |||||
| let chartOptions = options; | let chartOptions = options; | ||||
| let renderSeries: ApexChartProps["series"] = sanitized; | let renderSeries: ApexChartProps["series"] = sanitized; | ||||
| let showPlaceholder = shouldShowPlaceholder(type, options, sanitized, allowZeroSeries); | |||||
| if (isRadialChart(type) && isNumberArray(sanitized)) { | |||||
| if (!showPlaceholder && isRadialChart(type) && isNumberArray(sanitized)) { | |||||
| const prev = Array.isArray(options?.labels) ? (options!.labels as unknown[]) : []; | const prev = Array.isArray(options?.labels) ? (options!.labels as unknown[]) : []; | ||||
| const pairs = sanitized.map((v, i) => { | const pairs = sanitized.map((v, i) => { | ||||
| const n = Number.isFinite(v) ? v : 0; | const n = Number.isFinite(v) ? v : 0; | ||||
| @@ -183,20 +204,31 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||||
| }); | }); | ||||
| const nonzero = pairs.filter((p) => p.v > 0); | const nonzero = pairs.filter((p) => p.v > 0); | ||||
| if (nonzero.length === 0) { | if (nonzero.length === 0) { | ||||
| return ( | |||||
| <Typography color="text.secondary" sx={{ py: 3 }}> | |||||
| {EMPTY_MESSAGE} | |||||
| </Typography> | |||||
| ); | |||||
| showPlaceholder = true; | |||||
| } else { | |||||
| renderSeries = nonzero.map((p) => p.v); | |||||
| chartOptions = { ...options, labels: nonzero.map((p) => p.label) }; | |||||
| } | } | ||||
| renderSeries = nonzero.map((p) => p.v); | |||||
| chartOptions = { ...options, labels: nonzero.map((p) => p.label) }; | |||||
| } | } | ||||
| const chartType = String(type ?? "line"); | const chartType = String(type ?? "line"); | ||||
| const configSnapshot = `${String(chartRevision ?? "")}|${chartType}|${JSON.stringify(renderSeries ?? null)}|${JSON.stringify(chartOptions ?? {})}|${String(height ?? "")}|${String(width ?? "")}`; | |||||
| const configSnapshot = [ | |||||
| String(chartRevision ?? ""), | |||||
| chartType, | |||||
| String(showPlaceholder), | |||||
| safeStringify(renderSeries ?? null), | |||||
| safeStringify(cats ?? null), | |||||
| safeStringify(chartOptions?.labels ?? null), | |||||
| String(height ?? ""), | |||||
| String(width ?? ""), | |||||
| ].join("|"); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (showPlaceholder) { | |||||
| destroyChart(chartRef); | |||||
| return; | |||||
| } | |||||
| const el = containerRef.current; | const el = containerRef.current; | ||||
| if (!el) return; | if (!el) return; | ||||
| @@ -219,12 +251,7 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||||
| if (disposed || containerRef.current !== el) { | if (disposed || containerRef.current !== el) { | ||||
| if (chartRef.current === instance) { | if (chartRef.current === instance) { | ||||
| try { | |||||
| instance.destroy(); | |||||
| } catch { | |||||
| /* ignore */ | |||||
| } | |||||
| chartRef.current = null; | |||||
| destroyChart(chartRef); | |||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -236,21 +263,21 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||||
| return () => { | return () => { | ||||
| disposed = true; | disposed = true; | ||||
| const c = chartRef.current; | |||||
| chartRef.current = null; | |||||
| if (c) { | |||||
| try { | |||||
| c.destroy(); | |||||
| } catch { | |||||
| /* ignore */ | |||||
| } | |||||
| } | |||||
| destroyChart(chartRef); | |||||
| }; | }; | ||||
| }, [configSnapshot]); | }, [configSnapshot]); | ||||
| const minH = typeof height === "number" ? height : typeof height === "string" ? height : 240; | const minH = typeof height === "number" ? height : typeof height === "string" ? height : 240; | ||||
| const dom = rest as { className?: string; id?: string; style?: CSSProperties; sx?: object }; | const dom = rest as { className?: string; id?: string; style?: CSSProperties; sx?: object }; | ||||
| if (showPlaceholder) { | |||||
| return ( | |||||
| <Typography color="text.secondary" sx={{ py: 3 }}> | |||||
| {EMPTY_MESSAGE} | |||||
| </Typography> | |||||
| ); | |||||
| } | |||||
| return ( | return ( | ||||
| <Box | <Box | ||||
| ref={containerRef} | ref={containerRef} | ||||
| @@ -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,47 @@ | |||||
| "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", | |||||
| "Shop Code": "Shop group", | |||||
| "Shop code": "Shop code", | |||||
| "Back to previous level": "Back", | |||||
| "All lanes": "All lanes", | |||||
| "All shops": "All shop groups", | |||||
| "Cartons by lane": "Cartons by lane", | |||||
| "Cartons by shop": "Cartons by shop", | |||||
| "Cartons by shop group": "Cartons by shop group", | |||||
| "Cartons by floor": "Cartons by floor", | |||||
| "Share": "Share", | |||||
| "Click a row to filter": "Click a row to filter", | |||||
| "Click a bar to filter": "Click a bar to filter", | |||||
| "Reset filters": "Reset filters", | |||||
| "Cartons": "Cartons", | |||||
| "Date": "Date", | |||||
| "Download Excel": "Download Excel", | |||||
| "Download this view Excel": "Download Excel (Selected)", | |||||
| "Download period Excel": "Download Excel (Complete)", | |||||
| "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", | |||||
| "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", | |||||
| "Filtered": "Filtered", | |||||
| "Breakdown": "Breakdown", | |||||
| "All floors": "All floors", | |||||
| "FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{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,100 @@ | |||||
| "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", | |||||
| "Download this view Excel": "Download Excel (Selected)", | |||||
| "Download period Excel": "Download Excel (Complete)", | |||||
| "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", | |||||
| "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", | |||||
| "Filtered": "Filtered", | |||||
| "Breakdown": "Breakdown", | |||||
| "FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{date}}", | |||||
| "Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)", | |||||
| "2/F or 4/F": "2/F or 4/F", | |||||
| "Lane": "Lane", | |||||
| "Shop Code": "Shop group", | |||||
| "Shop code": "Shop code", | |||||
| "Back to previous level": "Back", | |||||
| "All lanes": "All lanes", | |||||
| "All shops": "All shop groups", | |||||
| "Cartons by lane": "Cartons by lane", | |||||
| "Cartons by shop": "Cartons by shop", | |||||
| "Cartons by shop group": "Cartons by shop group", | |||||
| "Cartons by floor": "Cartons by floor", | |||||
| "Share": "Share", | |||||
| "Click a row to filter": "Click a row to filter", | |||||
| "Click a bar to filter": "Click a bar to filter", | |||||
| "Reset filters": "Reset filters", | |||||
| "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,42 @@ | |||||
| "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": "車線", | |||||
| "Shop Code": "店鋪組別", | |||||
| "Shop code": "店鋪編號", | |||||
| "Back to previous level": "返回上一層", | |||||
| "All lanes": "全部車線", | |||||
| "All shops": "全部店鋪組別", | |||||
| "Cartons by lane": "按車線統計箱數", | |||||
| "Cartons by shop": "按店鋪編號統計箱數", | |||||
| "Cartons by shop group": "按店鋪組別統計箱數", | |||||
| "Cartons by floor": "按樓層統計箱數", | |||||
| "Share": "佔比", | |||||
| "Click a row to filter": "點選列以篩選", | |||||
| "Click a bar to filter": "點選長條以篩選", | |||||
| "Reset filters": "重設篩選", | |||||
| "Cartons": "箱數", | |||||
| "Date": "日期", | |||||
| "Download Excel": "下載 Excel", | |||||
| "Download this view Excel": "下載 Excel(已選)", | |||||
| "Download period Excel": "下載 Excel(完整)", | |||||
| "Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。", | |||||
| "Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。", | |||||
| "Filtered": "篩選", | |||||
| "Breakdown": "分項", | |||||
| "All floors": "全部樓層", | |||||
| "FG carton qty filtered title": "成品出倉出箱數量(篩選)- {{floor}} - {{lane}} - {{shop}} - {{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,102 @@ | |||||
| "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", | |||||
| "Download this view Excel": "下載 Excel(已選)", | |||||
| "Download period Excel": "下載 Excel(完整)", | |||||
| "Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。", | |||||
| "Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。", | |||||
| "Filtered": "篩選", | |||||
| "Breakdown": "分項", | |||||
| "FG carton qty filtered title": "成品出倉出箱數量(篩選)- {{floor}} - {{lane}} - {{shop}} - {{date}}", | |||||
| "Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)", | |||||
| "2/F or 4/F": "2/F 或 4/F", | |||||
| "Lane": "車線", | |||||
| "Shop Code": "店鋪組別", | |||||
| "Shop code": "店鋪編號", | |||||
| "Back to previous level": "返回上一層", | |||||
| "All lanes": "全部車線", | |||||
| "All shops": "全部店鋪組別", | |||||
| "Cartons by lane": "按車線統計箱數", | |||||
| "Cartons by shop": "按店鋪編號統計箱數", | |||||
| "Cartons by shop group": "按店鋪組別統計箱數", | |||||
| "Cartons by floor": "按樓層統計箱數", | |||||
| "Share": "佔比", | |||||
| "Click a row to filter": "點選列以篩選", | |||||
| "Click a bar to filter": "點選長條以篩選", | |||||
| "Reset filters": "重設篩選", | |||||
| "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") }; | |||||
| } | } | ||||