From 96c8ac643b8ee5a090f550da924fc65b1186bdbd Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Sat, 13 Jun 2026 01:26:18 +0800 Subject: [PATCH] replenishment update --- src/app/api/do/actions.tsx | 6 + .../DoSearch/DoReplenishmentTab.tsx | 1399 ++++++++++------- .../DoSearch/ReplenishmentFilterField.tsx | 265 +++- src/i18n/en/do.json | 40 +- src/i18n/zh/do.json | 40 +- 5 files changed, 1127 insertions(+), 623 deletions(-) diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index c6c6e0e..549f218 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -38,6 +38,8 @@ export interface DoDetailLine { id: number; itemNo: string; qty: number; + /** Sum of stock_out_line qty for linked pick order line; falls back to qty. */ + actualShippedQty?: number; price: number; status: string; itemName?: string; @@ -680,6 +682,7 @@ export interface SubmitDoReplenishmentLineRequest { sourceDoLineId: number; replenishQty: number; truckLaneCode?: string; + reason?: string; } export interface DoReplenishmentRecord { @@ -692,6 +695,7 @@ export interface DoReplenishmentRecord { itemId: number; itemNo?: string; itemName?: string; + originalQty?: number; replenishQty: number; shortUom?: string; shopCode?: string; @@ -699,8 +703,10 @@ export interface DoReplenishmentRecord { truckLaneCode?: string; targetDoId?: number; targetDoCode?: string; + targetDoEstimatedArrivalDate?: string; pickOrderLineId?: number; status: string; + reason?: string; created?: string; } diff --git a/src/components/DoSearch/DoReplenishmentTab.tsx b/src/components/DoSearch/DoReplenishmentTab.tsx index 1df160d..4e66eea 100644 --- a/src/components/DoSearch/DoReplenishmentTab.tsx +++ b/src/components/DoSearch/DoReplenishmentTab.tsx @@ -5,6 +5,7 @@ import { Autocomplete, Box, Button, + Collapse, Dialog, DialogContent, DialogTitle, @@ -14,13 +15,7 @@ import { Paper, Select, Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, TablePagination, - TableRow, Tooltip, Typography, } from "@mui/material"; @@ -29,12 +24,13 @@ import FilterListIcon from "@mui/icons-material/FilterList"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import ReceiptLongIcon from "@mui/icons-material/ReceiptLong"; import StorefrontIcon from "@mui/icons-material/Storefront"; -import { Add, Close, Delete, Search } from "@mui/icons-material"; +import { Add, Close, Delete, KeyboardArrowDown, KeyboardArrowRight } from "@mui/icons-material"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; import { useTranslation } from "react-i18next"; -import Swal from "sweetalert2"; +import type { TFunction } from "i18next"; +import Swal, { type SweetAlertOptions } from "sweetalert2"; import { DoDetail, DoDetailLine, @@ -47,27 +43,50 @@ import { import { arrayToDateString } from "@/app/utils/formatUtil"; import { REPLENISHMENT_FIELD_ICON_SX, + REPLENISHMENT_AUTOCOMPLETE_SX, REPLENISHMENT_FILLED_SELECT_SX, - REPLENISHMENT_TABLE_AUTOCOMPLETE_SX, - REPLENISHMENT_TABLE_ENTRY_ROW_SX, - REPLENISHMENT_TABLE_INLINE_TEXTFIELD_SX, REPLENISHMENT_TABLE_SX, + REPLENISHMENT_FIELD_BUTTON_SX, REPLENISHMENT_LOOKUP_BUTTON_SX, REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX, REPLENISHMENT_SOURCE_HEADER_SX, - REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX, REPLENISHMENT_TEXTFIELD_SX, - ReplenishmentFieldLabel, + ReplenishmentFieldLabelSpacer, ReplenishmentFilterField, - ReplenishmentItemEntryPlainText, + ReplenishmentReadonlyValue, ReplenishmentTextField, - replenishmentSearchGridInputSx, - replenishmentSearchGridLabelSx, - replenishmentSearchGridShopRowSx, } from "./ReplenishmentFilterField"; export type ReplenishmentStatus = "pending" | "processing" | "completed"; +export type ReplenishmentReasonCode = "quality_issue" | "out_of_stock" | "other"; + +const REPLENISHMENT_REASON_CODES: readonly ReplenishmentReasonCode[] = [ + "quality_issue", + "out_of_stock", + "other", +]; + +function isReplenishmentReasonCode(value: string): value is ReplenishmentReasonCode { + return (REPLENISHMENT_REASON_CODES as readonly string[]).includes(value); +} + +function formatReplenishmentReason(reason: string | undefined, t: TFunction): string { + const trimmed = reason?.trim(); + if (!trimmed) return "—"; + if (isReplenishmentReasonCode(trimmed)) { + return t(`replenishmentReason.${trimmed}`); + } + return trimmed; +} + +function fireReplenishmentAlert(t: TFunction, options: SweetAlertOptions) { + return Swal.fire({ + confirmButtonText: t("Confirm"), + ...options, + }); +} + export type ReplenishmentDraftRow = { rowId: string; deliveryDate: string; @@ -83,6 +102,7 @@ export type ReplenishmentDraftRow = { shopCode?: string; shopName?: string; truckLaneCode?: string; + reason?: string; }; export type ReplenishmentRecord = ReplenishmentDraftRow & { @@ -90,11 +110,68 @@ export type ReplenishmentRecord = ReplenishmentDraftRow & { code: string; targetDoId?: number; targetDoCode?: string; + targetDoEstimatedArrivalDate?: string; pickOrderLineId?: number; status: ReplenishmentStatus; created: string; }; +function resolveOriginalShipmentQty(line: DoDetailLine): number { + if (line.actualShippedQty != null && Number.isFinite(Number(line.actualShippedQty))) { + return Number(line.actualShippedQty); + } + return line.qty ?? 0; +} + +function formatQtyWithUom(qty: number | undefined, uom?: string): string { + const unit = uom?.trim(); + if (qty == null || Number.isNaN(qty)) { + return unit ? `— ${unit}` : "—"; + } + return unit ? `${qty} ${unit}` : String(qty); +} + +function TrackingFieldBlock({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) { + return ( + + + {label} + + + {value ?? "—"} + + + ); +} + +function TrackingInlineLine({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( + + + {label}: + {" "} + {children} + + ); +} + type SourceDoContext = { doId: number; doCode: string; @@ -115,7 +192,7 @@ function mapApiRecord(record: DoReplenishmentRecord): ReplenishmentRecord { itemId: record.itemId, itemNo: record.itemNo ?? "", itemName: record.itemName ?? "", - originalQty: 0, + originalQty: record.originalQty != null ? Number(record.originalQty) : 0, replenishQty: Number(record.replenishQty), shortUom: record.shortUom, shopCode: record.shopCode, @@ -127,6 +204,7 @@ function mapApiRecord(record: DoReplenishmentRecord): ReplenishmentRecord { targetDoCode: record.targetDoCode, pickOrderLineId: record.pickOrderLineId, status: record.status as ReplenishmentStatus, + reason: record.reason, created: record.created ?? "", }; } @@ -166,10 +244,27 @@ type DraftShopGroup = { dos: DraftDoGroup[]; }; +type DraftLaneGroup = { + laneKey: string; + truckLaneCode: string; + shops: DraftShopGroup[]; +}; + function draftShopGroupKey(row: ReplenishmentDraftRow): string { return row.shopCode?.trim() || row.shopName?.trim() || "—"; } +function draftLaneGroupKey(row: ReplenishmentDraftRow): string { + return row.truckLaneCode?.trim() || ""; +} + +function formatDraftShopDisplay(shopGroup: DraftShopGroup): string { + const name = shopGroup.shopName?.trim(); + const code = shopGroup.shopCode?.trim(); + if (code && name) return `${code} - ${name}`; + return name || code || shopGroup.shopKey; +} + function groupDraftRowsByShopAndDo(rows: ReplenishmentDraftRow[]): DraftShopGroup[] { const shopMap = new Map(); @@ -213,6 +308,126 @@ function groupDraftRowsByShopAndDo(rows: ReplenishmentDraftRow[]): DraftShopGrou })); } +function draftLaneCollapseKey(laneKey: string): string { + return laneKey || "__truck_x__"; +} + +function draftShopCollapseKey(laneKey: string, shopKey: string): string { + return `${draftLaneCollapseKey(laneKey)}::${shopKey}`; +} + +function draftDoCollapseKey(laneKey: string, shopKey: string, sourceDoId: number): string { + return `${draftShopCollapseKey(laneKey, shopKey)}::${sourceDoId}`; +} + +function toggleCollapsedKey( + setter: React.Dispatch>>, + key: string, +) { + setter((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); +} + +type DraftPreviewCollapseHeaderProps = { + collapsed: boolean; + onToggle: () => void; + label: string; + title: React.ReactNode; + titleVariant?: "subtitle2" | "body2"; + titleTooltip?: string; +}; + +function DraftPreviewCollapseHeader({ + collapsed, + onToggle, + label, + title, + titleVariant = "subtitle2", + titleTooltip, +}: DraftPreviewCollapseHeaderProps) { + const titleNode = ( + + {title} + + ); + + return ( + + { + event.stopPropagation(); + onToggle(); + }} + sx={{ p: 0.25, mt: -0.25, flexShrink: 0 }} + aria-expanded={!collapsed} + > + {collapsed ? ( + + ) : ( + + )} + + + + {label} + + {titleTooltip ? ( + + {titleNode} + + ) : ( + titleNode + )} + + + ); +} + +function groupDraftRowsByLaneShopAndDo(rows: ReplenishmentDraftRow[]): DraftLaneGroup[] { + const laneBuckets = new Map(); + + for (const row of rows) { + const laneKey = draftLaneGroupKey(row); + const bucket = laneBuckets.get(laneKey) ?? []; + bucket.push(row); + laneBuckets.set(laneKey, bucket); + } + + return Array.from(laneBuckets.entries()) + .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true })) + .map(([laneKey, laneRows]) => ({ + laneKey, + truckLaneCode: laneKey, + shops: groupDraftRowsByShopAndDo(laneRows), + })); +} + const DoReplenishmentTab: React.FC = () => { const { t } = useTranslation("do"); const inFlightRef = useRef(false); @@ -226,8 +441,14 @@ const DoReplenishmentTab: React.FC = () => { const [sourceDo, setSourceDo] = useState(null); const [selectedLine, setSelectedLine] = useState(null); const [replenishQtyInput, setReplenishQtyInput] = useState(""); + const [replenishReasonInput, setReplenishReasonInput] = useState( + "", + ); const [draftRows, setDraftRows] = useState([]); + const [collapsedLaneKeys, setCollapsedLaneKeys] = useState>(() => new Set()); + const [collapsedShopKeys, setCollapsedShopKeys] = useState>(() => new Set()); + const [collapsedDoKeys, setCollapsedDoKeys] = useState>(() => new Set()); const [records, setRecords] = useState([]); const [isLoadingTracking, setIsLoadingTracking] = useState(false); const [trackStatusFilter, setTrackStatusFilter] = useState("all"); @@ -242,18 +463,18 @@ const DoReplenishmentTab: React.FC = () => { const suffix = doCodeSuffix.trim(); const shop = shopInput.trim(); if (suffix.length !== 4) { - await Swal.fire({ + await fireReplenishmentAlert(t,{ icon: "warning", title: t("DO code suffix must be exactly 4 characters"), }); return; } if (!deliveryDateStr) { - await Swal.fire({ icon: "warning", title: t("Delivery date is required") }); + await fireReplenishmentAlert(t,{ icon: "warning", title: t("Delivery date is required") }); return; } if (!shop) { - await Swal.fire({ icon: "warning", title: t("Shop code or name is required") }); + await fireReplenishmentAlert(t,{ icon: "warning", title: t("Shop code or name is required") }); return; } setIsLookingUp(true); @@ -278,19 +499,19 @@ const DoReplenishmentTab: React.FC = () => { matchesDeliveryDate(r.estimatedArrivalDate, deliveryDateStr), ); if (candidates.length === 0) { - await Swal.fire({ icon: "error", title: t("Source DO not found") }); + await fireReplenishmentAlert(t,{ icon: "error", title: t("Source DO not found") }); setSourceDo(null); return; } const details = await Promise.all(candidates.map((c) => fetchDoDetail(c.id))); const matched = details.filter((d) => matchesShopInput(d, shop)); if (matched.length === 0) { - await Swal.fire({ icon: "error", title: t("Source DO not found") }); + await fireReplenishmentAlert(t,{ icon: "error", title: t("Source DO not found") }); setSourceDo(null); return; } if (matched.length > 1) { - await Swal.fire({ + await fireReplenishmentAlert(t,{ icon: "error", title: t("Multiple source DOs matched"), text: t("Please verify DO code suffix, delivery date and shop."), @@ -303,7 +524,7 @@ const DoReplenishmentTab: React.FC = () => { const resolvedTruckLaneCode = detail.truckLaneCode?.trim() || matchedCandidate?.truckLanceCode?.trim() || null; if (detail.status !== "completed") { - await Swal.fire({ + await fireReplenishmentAlert(t,{ icon: "error", title: t("Source DO must be completed"), text: t("Only completed delivery orders can be used as replenishment source."), @@ -322,8 +543,9 @@ const DoReplenishmentTab: React.FC = () => { }); setSelectedLine(null); setReplenishQtyInput(""); + setReplenishReasonInput(""); } catch { - await Swal.fire({ icon: "error", title: t("Failed to lookup source DO") }); + await fireReplenishmentAlert(t,{ icon: "error", title: t("Failed to lookup source DO") }); } finally { setIsLookingUp(false); } @@ -331,20 +553,20 @@ const DoReplenishmentTab: React.FC = () => { const handleAddDraftRow = useCallback(() => { if (!sourceDo) { - void Swal.fire({ icon: "warning", title: t("Please lookup source DO first") }); + void fireReplenishmentAlert(t,{ icon: "warning", title: t("Please lookup source DO first") }); return; } if (!deliveryDateStr) { - void Swal.fire({ icon: "warning", title: t("Delivery date is required") }); + void fireReplenishmentAlert(t,{ icon: "warning", title: t("Delivery date is required") }); return; } if (!selectedLine) { - void Swal.fire({ icon: "warning", title: t("Please select an item") }); + void fireReplenishmentAlert(t,{ icon: "warning", title: t("Please select an item") }); return; } const qty = Number(replenishQtyInput); if (!Number.isFinite(qty) || qty <= 0) { - void Swal.fire({ icon: "warning", title: t("Replenish qty must be greater than zero") }); + void fireReplenishmentAlert(t,{ icon: "warning", title: t("Replenish qty must be greater than zero") }); return; } const line = selectedLine; @@ -353,11 +575,16 @@ const DoReplenishmentTab: React.FC = () => { (r) => r.sourceDoLineId === line.id && r.sourceDoId === sourceDo.doId, ); + const reason = replenishReasonInput || undefined; if (existingRowIndex >= 0) { setDraftRows((prev) => prev.map((row, index) => index === existingRowIndex - ? { ...row, replenishQty: row.replenishQty + qty } + ? { + ...row, + replenishQty: row.replenishQty + qty, + reason: row.reason?.trim() || reason, + } : row, ), ); @@ -372,22 +599,25 @@ const DoReplenishmentTab: React.FC = () => { sourceDoLineId: line.id, itemNo: line.itemNo ?? "", itemName: line.itemName ?? line.itemNo ?? "", - originalQty: line.qty ?? 0, + originalQty: resolveOriginalShipmentQty(line), replenishQty: qty, shortUom: lineUomDisplay(line) || undefined, shopCode: sourceDo.shopCode, shopName: sourceDo.shopName, truckLaneCode: sourceDo.truckLaneCode?.trim() || undefined, + reason, }, ]); } setSelectedLine(null); setReplenishQtyInput(""); + setReplenishReasonInput(""); window.setTimeout(() => itemCodeInputRef.current?.focus(), 0); }, [ deliveryDateStr, draftRows, replenishQtyInput, + replenishReasonInput, selectedLine, sourceDo, t, @@ -399,12 +629,15 @@ const DoReplenishmentTab: React.FC = () => { const handleClearDraftRows = useCallback(() => { setDraftRows([]); + setCollapsedLaneKeys(new Set()); + setCollapsedShopKeys(new Set()); + setCollapsedDoKeys(new Set()); }, []); const handleSubmit = useCallback(async () => { if (inFlightRef.current) return; if (draftRows.length === 0) { - await Swal.fire({ icon: "warning", title: t("No draft rows to submit") }); + await fireReplenishmentAlert(t,{ icon: "warning", title: t("No draft rows to submit") }); return; } inFlightRef.current = true; @@ -417,10 +650,11 @@ const DoReplenishmentTab: React.FC = () => { sourceDoLineId: row.sourceDoLineId, replenishQty: row.replenishQty, truckLaneCode: row.truckLaneCode, + reason: row.reason?.trim() || undefined, })), ); setDraftRows([]); - await Swal.fire({ + await fireReplenishmentAlert(t,{ icon: "success", title: t("Replenishment submitted successfully"), text: created.map((row) => row.code).join(", "), @@ -428,7 +662,7 @@ const DoReplenishmentTab: React.FC = () => { } catch (error: unknown) { const message = error instanceof Error ? error.message : t("Failed to submit replenishment"); - await Swal.fire({ icon: "error", title: message }); + await fireReplenishmentAlert(t,{ icon: "error", title: message }); } finally { setIsSubmitting(false); inFlightRef.current = false; @@ -445,7 +679,7 @@ const DoReplenishmentTab: React.FC = () => { setRecords(data.map(mapApiRecord)); setTrackPage(0); } catch { - await Swal.fire({ icon: "error", title: t("Failed to load replenishment records") }); + await fireReplenishmentAlert(t,{ icon: "error", title: t("Failed to load replenishment records") }); } finally { setIsLoadingTracking(false); } @@ -458,11 +692,6 @@ const DoReplenishmentTab: React.FC = () => { }, [trackingDialogOpen, loadTrackingRecords]); const selectedLineUom = lineUomDisplay(selectedLine); - const sourceTruckLaneDisplay = sourceDo - ? sourceDo.truckLaneCode?.trim() - ? sourceDo.truckLaneCode - : t("Truck X") - : ""; const paginatedTrackRecords = useMemo(() => { const start = trackPage * trackRowsPerPage; @@ -483,8 +712,8 @@ const DoReplenishmentTab: React.FC = () => { [t], ); - const groupedDraftRows = useMemo( - () => groupDraftRowsByShopAndDo(draftRows), + const groupedDraftByLane = useMemo( + () => groupDraftRowsByLaneShopAndDo(draftRows), [draftRows], ); @@ -548,12 +777,13 @@ const DoReplenishmentTab: React.FC = () => { "& > *": { flexShrink: 0 }, }} > - {groupedDraftRows.map((shopGroup) => { - const shopDisplay = - shopGroup.shopCode || shopGroup.shopName?.trim() || shopGroup.shopKey; + {groupedDraftByLane.map((laneGroup) => { + const laneDisplay = laneGroup.truckLaneCode || t("Truck X"); + const laneCollapseKey = draftLaneCollapseKey(laneGroup.laneKey); + const laneCollapsed = collapsedLaneKeys.has(laneCollapseKey); return ( { theme.palette.mode === "dark" ? "grey.800" : "grey.50", }} > - - {t("Shop Code")} - - - - {shopDisplay} - - + toggleCollapsedKey(setCollapsedLaneKeys, laneCollapseKey)} + label={t("Truck Lane")} + title={laneDisplay} + titleTooltip={laneDisplay} + /> - - {shopGroup.dos.map((doGroup) => ( - ({ - border: `1px solid ${theme.palette.divider}`, - borderRadius: 1.5, - p: 1.25, - bgcolor: - theme.palette.mode === "dark" ? "grey.900" : "common.white", - })} - > - - {t("Delivery Order Code")} - - - - {doGroup.sourceDoCode} - - - - - {doGroup.rows.map((row) => { - const qtyLabel = row.shortUom - ? `${row.replenishQty} ${row.shortUom}` - : String(row.replenishQty); - return ( + + + {laneGroup.shops.map((shopGroup) => { + const shopDisplay = formatDraftShopDisplay(shopGroup); + const shopCode = shopGroup.shopCode?.trim(); + const shopCollapseKey = draftShopCollapseKey( + laneGroup.laneKey, + shopGroup.shopKey, + ); + const shopCollapsed = collapsedShopKeys.has(shopCollapseKey); + return ( + + theme.palette.mode === "dark" ? "grey.900" : "common.white", + }} + > + + toggleCollapsedKey(setCollapsedShopKeys, shopCollapseKey) + } + label={t("Shop Name")} + title={shopDisplay} + titleTooltip={ + shopCode && shopGroup.shopName?.trim() ? shopCode : undefined + } + /> + + + + {shopGroup.dos.map((doGroup) => { + const doCollapseKey = draftDoCollapseKey( + laneGroup.laneKey, + shopGroup.shopKey, + doGroup.sourceDoId, + ); + const doCollapsed = collapsedDoKeys.has(doCollapseKey); + return ( ({ - position: "relative", - pr: 4, - py: 0.75, - px: 1, - borderRadius: 1, + border: `1px solid ${theme.palette.divider}`, + borderRadius: 1.5, + p: 1.25, bgcolor: - theme.palette.mode === "dark" - ? "grey.800" - : "grey.50", + theme.palette.mode === "dark" ? "grey.800" : "grey.50", })} > - handleRemoveDraftRow(row.rowId)} - aria-label={t("Delete")} - sx={{ position: "absolute", top: 2, right: 2 }} - > - - - - - - {row.itemNo} - - - {row.itemName} - - + + toggleCollapsedKey(setCollapsedDoKeys, doCollapseKey) + } + label={t("Delivery Order Code")} + title={doGroup.sourceDoCode} + titleVariant="body2" + titleTooltip={doGroup.sourceDoCode} + /> + + + + {doGroup.rows.map((row) => { + const qtyLabel = row.shortUom + ? `${row.replenishQty} ${row.shortUom}` + : String(row.replenishQty); + return ( + ({ + display: "flex", + alignItems: "flex-start", + gap: 1, + py: 0.75, + px: 1, + borderRadius: 1, + bgcolor: + theme.palette.mode === "dark" + ? "grey.900" + : "common.white", + })} + > + + + + {row.itemNo} + + + {row.itemName} + + - - - {t("Replenish Qty")}:{" "} - - {qtyLabel} - + + + {t("Replenish Qty")}:{" "} + + {qtyLabel} + + {row.reason?.trim() ? ( + + + {t("Remark")}:{" "} + + {formatReplenishmentReason(row.reason, t)} + + ) : null} + + + handleRemoveDraftRow(row.rowId)} + aria-label={t("Delete")} + sx={{ flexShrink: 0, mt: -0.25 }} + > + + + + ); + })} + + - ); - })} - - - ))} - + ); + })} + + + + ); + })} + + ); })} @@ -687,10 +970,20 @@ const DoReplenishmentTab: React.FC = () => { {draftRows.length > 0 && ( - - @@ -703,7 +996,7 @@ const DoReplenishmentTab: React.FC = () => { { variant="outlined" sx={{ p: 2, + minWidth: 0, bgcolor: (theme) => (theme.palette.mode === "dark" ? "grey.900" : "grey.50"), }} > - - } - title={t("Estimated Arrival Date")} - required - sx={replenishmentSearchGridLabelSx(1)} - /> - - - { - setDeliveryDate(v); + + + } + title={t("Estimated Arrival Date")} + required + > + + { + setDeliveryDate(v); + setSourceDo(null); + }} + slotProps={datePickerSlotProps} + sx={{ width: "100%" }} + /> + + + + } + title={t("DO Code Last 4")} + required + > + { + setDoCodeSuffix(e.target.value.slice(0, 4)); setSourceDo(null); }} - slotProps={datePickerSlotProps} - sx={{ width: "100%" }} + placeholder={t("replenishmentDoSuffixPlaceholder")} + inputProps={{ maxLength: 4 }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + void handleLookupSourceDo(); + } + }} /> - - - - } - title={t("DO Code Last 4")} - required - sx={replenishmentSearchGridLabelSx(2)} - /> - - { - setDoCodeSuffix(e.target.value.slice(0, 4)); - setSourceDo(null); - }} - placeholder={t("replenishmentDoSuffixPlaceholder")} - inputProps={{ maxLength: 4 }} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - void handleLookupSourceDo(); - } - }} - /> - + - } - title={t("Shop Code")} - required - sx={replenishmentSearchGridLabelSx(3)} - /> - - { - setShopInput(e.target.value); - setSourceDo(null); - }} - placeholder={t("replenishmentShopPlaceholder")} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - void handleLookupSourceDo(); - } + - - - - + { + setShopInput(e.target.value); + setSourceDo(null); + }} + placeholder={t("replenishmentShopPlaceholder")} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + void handleLookupSourceDo(); + } + }} + /> + + - + + + + + + {sourceDo && ( - - {t("Delivery Order Code")}: {sourceDo.doCode} - {" "} - {t("Shop Name")}: {sourceDo.shopName ?? "—"} - {" "} - {t("Truck Lance Code")}:{" "} - {sourceDo.truckLaneCode?.trim() ? sourceDo.truckLaneCode : t("Truck X")} - + + + {t("Delivery Order Code")}: {sourceDo.doCode} + {" "} + {t("Shop Name")}: {sourceDo.shopName ?? "—"} + + + {t("Truck Lane")}:{" "} + {sourceDo.truckLaneCode?.trim() ? sourceDo.truckLaneCode : t("Truck X")} + + )} {sourceDo && ( - ({ border: `1px solid ${theme.palette.divider}`, borderRadius: 2, + p: 1.5, bgcolor: theme.palette.mode === "dark" ? "grey.900" : "common.white", })} > - - - - - {t("Replenishment item code")} - - {t("Item Name")} - - {t("Original Shipment Qty")} - - - {t("Replenish Qty")} - - - {t("uom")} - - - {t("Truck Lance Code")} - - - {t("Action")} - - - - - {currentDoDraftRows.map((row) => ( - ({ - bgcolor: - theme.palette.mode === "dark" - ? "action.selected" - : "action.hover", - })} - > - {row.itemNo} - {row.itemName} - {row.originalQty} - {row.replenishQty} - {row.shortUom || "—"} - - + + } + title={t("Replenishment item code")} + required + > + setSelectedLine(newValue)} + onInputChange={(_, inputValue, reason) => { + if (reason === "clear") { + setSelectedLine(null); + return; + } + if (reason === "input" && selectedLine) { + const selectedCode = (selectedLine.itemNo ?? "").trim(); + if (inputValue.trim() !== selectedCode) { + setSelectedLine(null); } - placement="top" - arrow - > - line.itemNo ?? ""} + isOptionEqualToValue={(a, b) => a.id === b.id} + filterOptions={(options, { inputValue }) => { + const query = inputValue.trim().toLowerCase(); + if (!query) return options; + return options.filter((line) => + (line.itemNo ?? "").toLowerCase().includes(query), + ); + }} + renderInput={(params) => { + const { inputProps } = params; + return ( + { + itemCodeInputRef.current = node; + const { ref } = inputProps; + if (typeof ref === "function") ref(node); + else if (ref) { + ( + ref as React.MutableRefObject + ).current = node; + } + }, }} - > - {row.truckLaneCode?.trim() || - sourceDo.truckLaneCode?.trim() || - t("Truck X")} - - - - - - handleRemoveDraftRow(row.rowId)} - aria-label={t("Delete")} - > - - - - - - ))} - - - setSelectedLine(newValue)} - getOptionLabel={(line) => line.itemNo ?? ""} - isOptionEqualToValue={(a, b) => a.id === b.id} - filterOptions={(options, { inputValue }) => { - const query = inputValue.trim().toLowerCase(); - if (!query) return options; - return options.filter((line) => - (line.itemNo ?? "").toLowerCase().includes(query), - ); - }} - renderInput={(params) => { - const { inputProps } = params; - return ( - { - itemCodeInputRef.current = node; - const { ref } = inputProps; - if (typeof ref === "function") ref(node); - else if (ref) { - ( - ref as React.MutableRefObject - ).current = node; - } - }, - }} - InputProps={{ ...params.InputProps, disableUnderline: true }} - /> - ); - }} - sx={REPLENISHMENT_TABLE_AUTOCOMPLETE_SX} - /> - - - + ); + }} + sx={REPLENISHMENT_AUTOCOMPLETE_SX} + /> + + + + + {selectedLine + ? (selectedLine.itemName ?? selectedLine.itemNo ?? "") + : null} + + + + + + + + {selectedLine + ? `${resolveOriginalShipmentQty(selectedLine)}${selectedLineUom ? ` ${selectedLineUom}` : ""}` + : null} + + + + + setReplenishQtyInput(e.target.value)} + placeholder={t("Replenish Qty")} + disabled={!selectedLine} + inputProps={{ min: 0, step: "any" }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddDraftRow(); } - /> - - - - - - - setReplenishQtyInput(e.target.value)} - inputProps={{ min: 0, step: "any", style: { textAlign: "right" } }} - sx={(theme) => ({ - ...REPLENISHMENT_TABLE_INLINE_TEXTFIELD_SX(theme), - width: 72, - "& .MuiFilledInput-input": { - textAlign: "right", - }, - })} - /> - - - - - - + + + + - - theme.spacing(5), - lineHeight: (theme) => theme.spacing(5), - }} - > - {sourceTruckLaneDisplay} - - - - - - - - - - -
-
+ + + + + + + + +
+ + + + {currentDoDraftRows.length > 0 ? ( + + {t("replenishmentCurrentDoDraftHint", { count: currentDoDraftRows.length })} + + ) : null} )} - + {draftPreviewPanel} - {draftPreviewPanel} + {draftPreviewPanel} { > } - title={t("Estimated Arrival Date")} + title={t("replenishmentTargetDoEstimatedArrivalDate")} > { - ({ - border: `1px solid ${theme.palette.divider}`, borderRadius: 2, bgcolor: theme.palette.mode === "dark" ? "grey.900" : "common.white", opacity: isLoadingTracking ? 0.6 : 1, pointerEvents: isLoadingTracking ? "none" : "auto", + display: "flex", + flexDirection: "column", + maxHeight: "65vh", })} > - - - - {t("Replenishment Code")} - {t("Source DO")} - {t("Shop Name")} - {t("Truck Lance Code")} - {t("Item No.")} - {t("Item Name")} - - {t("Replenish Qty")} - - {t("uom")} - {t("Target DO")} - {t("Status")} - {t("Created")} - - - - {paginatedTrackRecords.length === 0 ? ( - - - {isLoadingTracking ? t("Loading") : t("No data")} - - - ) : ( - paginatedTrackRecords.map((row) => ( - - {row.code} - {row.sourceDoCode} - {row.shopName ?? "—"} - - {row.truckLaneCode ?? "—"} - - {row.itemNo} - {row.itemName} - {row.replenishQty} - {row.shortUom ?? "—"} - {row.targetDoCode ?? "—"} - {t(row.status)} - - {row.created - ? dayjs(row.created).format("YYYY-MM-DD HH:mm") - : "—"} - - - )) - )} - -
+ *": { flexShrink: 0 }, + }} + > + {paginatedTrackRecords.length === 0 ? ( + + {isLoadingTracking ? t("Loading") : t("No data")} + + ) : ( + paginatedTrackRecords.map((row) => ( + ({ + p: 2, + borderRadius: 1.5, + flexShrink: 0, + overflow: "visible", + bgcolor: + theme.palette.mode === "dark" ? "grey.800" : "grey.50", + })} + > + + + + + + + + + + + + {formatQtyWithUom(row.originalQty, row.shortUom)} + + + {formatQtyWithUom(row.replenishQty, row.shortUom)} + + + {formatReplenishmentReason(row.reason, t)} + + + + + + + + + + + )) + )} + { }} rowsPerPageOptions={[10, 25, 50]} labelRowsPerPage={t("Rows per page")} + sx={{ flexShrink: 0, borderTop: 1, borderColor: "divider" }} /> -
+
diff --git a/src/components/DoSearch/ReplenishmentFilterField.tsx b/src/components/DoSearch/ReplenishmentFilterField.tsx index 369b3cd..2661851 100644 --- a/src/components/DoSearch/ReplenishmentFilterField.tsx +++ b/src/components/DoSearch/ReplenishmentFilterField.tsx @@ -11,7 +11,7 @@ export const REPLENISHMENT_FIELD_LABEL_SX = (theme: Theme) => ({ theme.palette.mode === "dark" ? theme.palette.grey[100] : theme.palette.common.black, - fontWeight: 600, + fontWeight: 700, }); export const REPLENISHMENT_FIELD_ICON_SX = (theme: Theme) => ({ @@ -28,6 +28,7 @@ export const REPLENISHMENT_TEXTFIELD_SX = (theme: Theme) => borderRadius: 2, bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", border: `1px solid ${theme.palette.divider}`, + ...REPLENISHMENT_FIELD_CONTROL_ROOT_SX, }, "& .MuiFilledInput-root.Mui-focused": { bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", @@ -56,6 +57,7 @@ export const REPLENISHMENT_AUTOCOMPLETE_SX = (theme: Theme) => width: "100%", }, "& .MuiAutocomplete-inputRoot": { + ...REPLENISHMENT_FIELD_CONTROL_ROOT_SX, paddingTop: `${REPLENISHMENT_FIELD_BODY_PY} !important`, paddingBottom: `${REPLENISHMENT_FIELD_BODY_PY} !important`, paddingLeft: `${theme.spacing(REPLENISHMENT_FIELD_BODY_PX)} !important`, @@ -75,6 +77,75 @@ export const REPLENISHMENT_FIELD_BODY_PY = "12px"; /** Horizontal padding aligned with MUI filled input (spacing 1.5 = 12px). */ export const REPLENISHMENT_FIELD_BODY_PX = 1.5; +/** Fixed height for replenishment inputs, selects, and read-only value boxes. */ +export const REPLENISHMENT_FIELD_CONTROL_HEIGHT = 44; + +export const REPLENISHMENT_FIELD_CONTROL_ROOT_SX = { + height: REPLENISHMENT_FIELD_CONTROL_HEIGHT, + minHeight: REPLENISHMENT_FIELD_CONTROL_HEIGHT, + maxHeight: REPLENISHMENT_FIELD_CONTROL_HEIGHT, + boxSizing: "border-box" as const, +}; + +/** Read-only value box — same outer height as {@link ReplenishmentTextField}. */ +export const REPLENISHMENT_READONLY_VALUE_SX = (theme: Theme) => + ({ + ...REPLENISHMENT_FIELD_CONTROL_ROOT_SX, + borderRadius: 2, + border: `1px solid ${theme.palette.divider}`, + bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", + px: REPLENISHMENT_FIELD_BODY_PX, + display: "flex", + alignItems: "center", + minWidth: 0, + overflow: "hidden", + }) as const; + +export function ReplenishmentReadonlyValue({ + children, + fontWeight, +}: { + children: React.ReactNode; + fontWeight?: number; +}) { + return ( + + + {children ?? "\u00A0"} + + + ); +} + +/** Invisible label spacer so action buttons align with labelled fields. */ +export function ReplenishmentFieldLabelSpacer() { + return ( + + {"\u00A0"} + + ); +} + /** Source DO summary header: same inset as textbox content area. */ export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) => ({ @@ -89,7 +160,7 @@ export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) => }) as const; type ReplenishmentFieldLabelProps = { - icon: ReactNode; + icon?: ReactNode; title: string; required?: boolean; sx?: SxProps; @@ -102,14 +173,22 @@ export function ReplenishmentFieldLabel({ sx, }: ReplenishmentFieldLabelProps) { return ( - - {icon} - + + {icon ?? null} + ({ + ...REPLENISHMENT_FIELD_LABEL_SX(theme), + whiteSpace: "normal", + lineHeight: 1.35, + })} + component="span" + > {title} {required ? ( - + ) : null} @@ -163,10 +242,13 @@ export const REPLENISHMENT_FILLED_SELECT_SX = (theme: Theme) => bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", border: `1px solid ${theme.palette.divider}`, "&::before, &::after": { display: "none" }, + ...REPLENISHMENT_FIELD_CONTROL_ROOT_SX, }, "& .MuiSelect-select": { paddingTop: REPLENISHMENT_FIELD_BODY_PY, paddingBottom: REPLENISHMENT_FIELD_BODY_PY, + display: "flex", + alignItems: "center", }, }) as const; @@ -242,17 +324,57 @@ export function ReplenishmentQtyWithUomField({ ); } +/** Tracking dialog table — horizontal scroll, no fixed layout (avoids column text stacking). */ +export const REPLENISHMENT_TRACKING_TABLE_SX = { + width: "max-content", + minWidth: "100%", + "& .MuiTableCell-root": { + typography: "body2", + borderColor: "divider", + py: 1, + px: 1.25, + whiteSpace: "nowrap", + }, + "& .MuiTableCell-root:first-of-type": { + pl: 1.5, + }, + "& .MuiTableHead-root .MuiTableCell-root": { + fontWeight: 600, + color: "text.secondary", + bgcolor: "action.hover", + borderBottom: "1px solid", + borderColor: "divider", + }, + "& .MuiTableBody-root .MuiTableRow-root:not(:last-of-type) .MuiTableCell-root": { + borderBottom: "1px solid", + borderColor: "divider", + }, +} as const; + +export const REPLENISHMENT_TRACKING_CELL_ELLIPSIS_SX = { + maxWidth: 160, + overflow: "hidden", + textOverflow: "ellipsis", +} as const; + +export const REPLENISHMENT_TRACKING_CELL_WRAP_SX = { + minWidth: 120, + maxWidth: 200, + whiteSpace: "normal", + wordBreak: "break-word", +} as const; + export const REPLENISHMENT_TABLE_SX = { - tableLayout: { md: "fixed" }, width: "100%", + tableLayout: "fixed", "& .MuiTableCell-root": { typography: "body2", borderColor: "divider", - py: 1.25, - px: 2, + py: 1, + px: 1.25, }, "& .MuiTableCell-root:first-of-type": { - pl: 3.5, + pl: 1.5, }, "& .MuiTableHead-root .MuiTableCell-root": { fontWeight: 600, @@ -378,10 +500,27 @@ export const REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX = { width: "100%", } as const; +/** In-table select — compact padding; truncate long selected labels. */ +export const REPLENISHMENT_TABLE_INLINE_SELECT_SX = (theme: Theme) => + ({ + ...REPLENISHMENT_FILLED_SELECT_SX(theme), + "& .MuiSelect-select": { + paddingTop: "6px", + paddingBottom: "6px", + paddingLeft: theme.spacing(1), + paddingRight: `${theme.spacing(3)} !important`, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + }) as const; + export const replenishmentSearchGridLabelSx = (col: number) => ({ gridColumn: { xs: 1, lg: col }, gridRow: { xs: "auto", lg: 1 }, - minWidth: 0, + minWidth: "min-content", + overflow: "hidden", + textOverflow: "ellipsis", }); export const replenishmentSearchGridInputSx = (col: number) => ({ @@ -395,66 +534,74 @@ export const replenishmentSearchGridInputSx = (col: number) => ({ }, }); -/** Shop input + lookup button share one row; button height follows the textbox. */ -export const replenishmentSearchGridShopRowSx = { - gridColumn: { xs: 1, lg: 3 }, +/** Lookup / tracking buttons beside the three filter inputs (4th grid column on lg). */ +export const replenishmentSearchGridActionsSx = { + gridColumn: { xs: 1, lg: 4 }, gridRow: { xs: "auto", lg: 2 }, - minWidth: 0, display: "flex", + justifyContent: { xs: "stretch", lg: "flex-start" }, alignItems: "stretch", - gap: 1, - "& .MuiTextField-root": { - flex: 1, - minWidth: 0, - }, - "& .MuiFormControl-root": { - height: "100%", - }, - "& .MuiFilledInput-root": { - height: "100%", - boxSizing: "border-box", + flexWrap: { xs: "wrap", lg: "nowrap" }, + gap: 1.5, + minWidth: 0, + "& .MuiButton-root": { + flex: { xs: 1, lg: "0 0 auto" }, + alignSelf: "stretch", }, }; -/** Match {@link ReplenishmentFieldLabel} typography on contained buttons. */ +/** Match {@link ReplenishmentFieldLabel} typography on field-height buttons. */ export const REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX = (theme: Theme) => ({ fontSize: theme.typography.body2.fontSize, fontWeight: 600, lineHeight: 1, }); -export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) => ({ - ...REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX(theme), - alignSelf: "stretch", - minHeight: "unset", - height: "auto", - py: 0, - px: 1.5, - borderRadius: 2, - boxShadow: "none", - textTransform: "none", - whiteSpace: "nowrap", - flexShrink: 0, - minWidth: { xs: "auto", lg: 108 }, - "& .MuiButton-startIcon": { - margin: 0, - marginRight: theme.spacing(0.75), - "& > *:nth-of-type(1)": { - fontSize: 20, +/** Base button style — same 44px height as {@link ReplenishmentTextField}. */ +export const REPLENISHMENT_FIELD_BUTTON_SX = (theme: Theme) => + ({ + ...REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX(theme), + ...REPLENISHMENT_FIELD_CONTROL_ROOT_SX, + paddingTop: 0, + paddingBottom: 0, + px: REPLENISHMENT_FIELD_BODY_PX, + borderRadius: 2, + boxShadow: "none", + textTransform: "none", + whiteSpace: "nowrap", + flexShrink: 0, + "&.MuiButton-root": { + ...REPLENISHMENT_FIELD_CONTROL_ROOT_SX, }, - }, -}); + "& .MuiButton-startIcon": { + margin: 0, + marginRight: theme.spacing(0.75), + "& > *:nth-of-type(1)": { + fontSize: 18, + }, + }, + }) as const; + +export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) => + ({ + ...REPLENISHMENT_FIELD_BUTTON_SX(theme), + alignSelf: "stretch", + px: REPLENISHMENT_FIELD_BODY_PX, + minWidth: { xs: "auto", lg: 108 }, + }) as const; /** Outlined companion button (e.g. replenishment tracking) beside lookup. */ -export const REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX = (theme: Theme) => ({ - ...REPLENISHMENT_LOOKUP_BUTTON_SX(theme), - minWidth: "auto", - borderColor: theme.palette.divider, - color: theme.palette.text.primary, - bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", - "&:hover": { - borderColor: theme.palette.primary.main, - bgcolor: theme.palette.mode === "dark" ? "grey.700" : "grey.50", - }, -}); +export const REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX = (theme: Theme) => + ({ + ...REPLENISHMENT_FIELD_BUTTON_SX(theme), + minWidth: "auto", + px: REPLENISHMENT_FIELD_BODY_PX, + borderColor: theme.palette.divider, + color: theme.palette.text.primary, + bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", + "&:hover": { + borderColor: theme.palette.primary.main, + bgcolor: theme.palette.mode === "dark" ? "grey.700" : "grey.50", + }, + }) as const; diff --git a/src/i18n/en/do.json b/src/i18n/en/do.json index 9d14976..3a3834c 100644 --- a/src/i18n/en/do.json +++ b/src/i18n/en/do.json @@ -73,27 +73,35 @@ "Delivery Date": "Delivery Date", "Enter last 4 characters of DO code": "Enter last 4 characters of DO code", "Shop code, or first characters of shop name": "Shop code (partial match), or first characters of shop name", - "Multiple source DOs matched": "Multiple source DOs matched", + "Multiple source DOs matched": "Multiple original DOs matched", "Please verify DO code suffix, delivery date and shop.": "Please verify DO code suffix, delivery date and shop.", "Shop code or name is required": "Shop code or name is required", "Draft List": "Draft List", - "Replenishment preview hint": "Add items from different source DOs, then batch submit from here.", - "Replenishment preview empty": "Added items appear here. Look up another source DO to keep adding.", + "Replenishment preview hint": "Add items from different original DOs, then batch submit from here.", + "Replenishment preview empty": "Added items appear here. Match another original DO to keep adding.", + "replenishmentCurrentDoDraftHint": "Added to draft list ({{count}} for this DO)", + "replenishmentTargetDoEstimatedArrivalDate": "Target DO Estimated Arrival Date", + "replenishmentOriginalSourceDoCode": "Original DO Code", + "replenishmentTargetDoCode": "Target DO Code", + "replenishmentStatusLabel": "Replenishment Status", + "replenishmentItemInfo": "Item Information", "Clear": "Clear", "Enter item code to search": "Enter item code to search", - "Failed to lookup source DO": "Failed to lookup source DO", + "Failed to lookup source DO": "Failed to match original DO", "Item": "Item", - "Lookup": "Lookup", + "Lookup": "Match DO", "No draft rows to submit": "No draft rows to submit", - "Only completed delivery orders can be used as replenishment source.": "Only completed delivery orders can be used as replenishment source.", + "Only completed delivery orders can be used as replenishment source.": "Only completed delivery orders can be used as the original DO.", "Original Shipment Qty": "Original Shipment Qty", - "Please lookup source DO first": "Please lookup source DO first", + "Original Shipment Qty short": "Orig. Qty", + "Please lookup source DO first": "Please match original DO first", "Picker Name": "Picker Name", "Please select an item": "Please select an item", "Records saved locally for preview. Backend integration pending.": "Records saved locally for preview. Backend integration pending.", "Replenishment Code": "Replenishment No.", "Ref Code": "Ref Code", "Replenish Qty": "Replenish Qty", + "Replenish Qty short": "Replenish", "Replenish qty must be greater than zero": "Replenish qty must be greater than zero", "Replenishment": "Replenishment", "Delivery date is required": "Delivery date is required", @@ -111,11 +119,18 @@ "replenishmentDatePlaceholder": "YYYY-MM-DD", "replenishmentDoSuffixPlaceholder": "DO No. (last 4)", "replenishmentShopPlaceholder": "Shop Code", - "Source DO": "Source DO", - "Source DO Code": "Source DO Code", - "Source DO code is required": "Source DO code is required", - "Source DO must be completed": "Source DO must be completed", - "Source DO not found": "Source DO not found", + "replenishmentRemarkPlaceholder": "Optional", + "replenishmentRemarkShort": "Optional", + "replenishmentReason": { + "quality_issue": "Quality issue", + "out_of_stock": "Out of stock", + "other": "Other" + }, + "Source DO": "Original DO", + "Source DO Code": "Original DO Code", + "Source DO code is required": "Original DO code is required", + "Source DO must be completed": "Original DO must be completed", + "Source DO not found": "Original DO not found", "Submit": "Submit", "Target DO": "Target DO", "This item is already in the draft list": "This item is already in the draft list", @@ -144,6 +159,7 @@ "Supplier Name": "Supplier Name", "Truck Availability Warning": "Truck Availability Warning", "Truck Lance Code": "Truck Lance Code", + "Truck Lane": "Truck Lane", "Truck X": "Truck X", "Truck lane search requires date message": "Truck lane search requires date message", "Truck lane search requires date title": "Truck lane search requires date title", diff --git a/src/i18n/zh/do.json b/src/i18n/zh/do.json index 4dce65b..221ba9f 100644 --- a/src/i18n/zh/do.json +++ b/src/i18n/zh/do.json @@ -19,26 +19,34 @@ "Enter last 4 characters of DO code": "請輸入送貨單號末四位", "Enter item code to search": "輸入貨品編號搜尋", "Shop code, or first characters of shop name": "店鋪代碼(部分符合),或店鋪名稱開頭字元", - "Multiple source DOs matched": "找到多張符合的來源送貨單", + "Multiple source DOs matched": "找到多張符合的原送貨單", "Please verify DO code suffix, delivery date and shop.": "請核對送貨單號末四位、送貨日及店鋪資料。", "Shop code or name is required": "請輸入店鋪代碼或名稱", "Draft List": "待提交列表", - "Replenishment preview hint": "可從不同來源送貨單加入品項,在此批次提交。", - "Replenishment preview empty": "加入的品項會顯示於此;可再查詢其他來源送貨單繼續加入。", + "Replenishment preview hint": "可從不同原送貨單加入品項,在此批次提交。", + "Replenishment preview empty": "加入的品項會顯示於此;可再對單其他原送貨單繼續加入。", + "replenishmentCurrentDoDraftHint": "已加入待提交列表(此送貨單 {{count}} 項)", + "replenishmentTargetDoEstimatedArrivalDate": "目標送貨單預計送貨日期", + "replenishmentOriginalSourceDoCode": "原送貨單編號", + "replenishmentTargetDoCode": "目標送貨單編號", + "replenishmentStatusLabel": "補貨狀態", + "replenishmentItemInfo": "貨品資訊", "Clear": "清空", - "Failed to lookup source DO": "查詢來源送貨單失敗", + "Failed to lookup source DO": "原送貨單對單失敗", "Item": "物品", - "Lookup": "查詢", + "Lookup": "對單", "No draft rows to submit": "沒有待提交的行", - "Only completed delivery orders can be used as replenishment source.": "只有已送貨(completed)的送貨單可作為補貨來源。", + "Only completed delivery orders can be used as replenishment source.": "只有已送貨(completed)的送貨單可作為原送貨單。", "Original Shipment Qty": "原出貨數", - "Please lookup source DO first": "請先查詢來源送貨單", + "Original Shipment Qty short": "原出貨", + "Please lookup source DO first": "請先對單(原送貨單)", "Picker Name": "揀貨員名稱", "Please select an item": "請選擇物品", "Records saved locally for preview. Backend integration pending.": "記錄已暫存於本地預覽,後端 API 尚未就緒。", "Replenishment Code": "補貨編號", "Ref Code": "參考編號", "Replenish Qty": "補貨數量", + "Replenish Qty short": "補貨", "Replenish qty must be greater than zero": "補貨數量必須大於零", "Replenishment": "補貨", "Delivery date is required": "請選擇送貨日期", @@ -56,11 +64,18 @@ "replenishmentDatePlaceholder": "YYYY-MM-DD", "replenishmentDoSuffixPlaceholder": "送貨單號末四位", "replenishmentShopPlaceholder": "店鋪編號", - "Source DO": "來源送貨單", - "Source DO Code": "來源送貨單編號", - "Source DO code is required": "請輸入來源送貨單編號", - "Source DO must be completed": "來源送貨單須為已送貨狀態", - "Source DO not found": "找不到來源送貨單", + "replenishmentRemarkPlaceholder": "請選擇(選填)", + "replenishmentRemarkShort": "選填", + "replenishmentReason": { + "quality_issue": "質素問題", + "out_of_stock": "缺貨", + "other": "其他" + }, + "Source DO": "原送貨單", + "Source DO Code": "原送貨單編號", + "Source DO code is required": "請輸入原送貨單編號", + "Source DO must be completed": "原送貨單須為已送貨狀態", + "Source DO not found": "找不到原送貨單", "Submit": "提交", "Target DO": "目標送貨單", "This item is already in the draft list": "此物品已在待提交列表中", @@ -83,6 +98,7 @@ "Truck lane search requires date title": "需選擇預計送貨日期", "Truck lane search requires date message": "已填寫車線號碼時,請一併選擇預計送貨日期後再搜索。", "Truck Lance Code": "車線號碼", + "Truck Lane": "車線", "Select Remark": "選擇備註", "Confirm Assignment": "確認分配", "Submit Qty": "提交數量",