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();
- }
+
-
- }
- onClick={() => void handleLookupSourceDo()}
- disabled={isLookingUp}
- sx={REPLENISHMENT_LOOKUP_BUTTON_SX}
- >
- {t("Lookup")}
-
- }
- onClick={() => setTrackingDialogOpen(true)}
- sx={REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX}
+ >
+ }
+ title={t("Shop Code")}
+ required
>
- {t("Replenishment Tracking")}
-
-
+ {
+ setShopInput(e.target.value);
+ setSourceDo(null);
+ }}
+ placeholder={t("replenishmentShopPlaceholder")}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ void handleLookupSourceDo();
+ }
+ }}
+ />
+
+
-
+
+
+ void handleLookupSourceDo()}
+ disabled={isLookingUp}
+ sx={REPLENISHMENT_LOOKUP_BUTTON_SX}
+ >
+ {t("Lookup")}
+
+ }
+ onClick={() => setTrackingDialogOpen(true)}
+ sx={REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX}
+ >
+ {t("Replenishment Tracking")}
+
+
+
{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}
-
-
-
-
-
- }
- onClick={handleAddDraftRow}
- sx={{
- borderRadius: 2,
- textTransform: "none",
- whiteSpace: "nowrap",
- px: 1.5,
- fontSize: (theme) => theme.typography.body2.fontSize,
- }}
- >
- {t("Add Row")}
-
-
-
-
-
-
-
+
+
+
+
+
+
+ }
+ onClick={handleAddDraftRow}
+ disabled={!selectedLine}
+ sx={REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX}
+ >
+ {t("Add Row")}
+
+
+
+
+
+
+ {currentDoDraftRows.length > 0 ? (
+
+ {t("replenishmentCurrentDoDraftHint", { count: currentDoDraftRows.length })}
+
+ ) : null}
)}
-
+
{draftPreviewPanel}
- {draftPreviewPanel}
+ {draftPreviewPanel}
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": "提交數量",