Преглед на файлове

補貨UI update

production
tommy преди 1 седмица
родител
ревизия
bde159732d
променени са 3 файла, в които са добавени 410 реда и са изтрити 57 реда
  1. +404
    -57
      src/components/DoSearch/DoReplenishmentTab.tsx
  2. +3
    -0
      src/i18n/en/do.json
  3. +3
    -0
      src/i18n/zh/do.json

+ 404
- 57
src/components/DoSearch/DoReplenishmentTab.tsx Целия файл

@@ -151,6 +151,66 @@ function lineUomDisplay(line?: DoDetailLine | null): string {
return (line.shortUom ?? line.uomCode ?? line.uom ?? "").trim();
}

type DraftDoGroup = {
sourceDoId: number;
sourceDoCode: string;
rows: ReplenishmentDraftRow[];
};

type DraftShopGroup = {
shopKey: string;
shopCode: string;
shopName?: string;
dos: DraftDoGroup[];
};

function draftShopGroupKey(row: ReplenishmentDraftRow): string {
return row.shopCode?.trim() || row.shopName?.trim() || "—";
}

function groupDraftRowsByShopAndDo(rows: ReplenishmentDraftRow[]): DraftShopGroup[] {
const shopMap = new Map<string, DraftShopGroup>();

for (const row of rows) {
const shopKey = draftShopGroupKey(row);
let shopGroup = shopMap.get(shopKey);
if (!shopGroup) {
shopGroup = {
shopKey,
shopCode: row.shopCode?.trim() || "",
shopName: row.shopName,
dos: [],
};
shopMap.set(shopKey, shopGroup);
}

let doGroup = shopGroup.dos.find((group) => group.sourceDoId === row.sourceDoId);
if (!doGroup) {
doGroup = {
sourceDoId: row.sourceDoId,
sourceDoCode: row.sourceDoCode,
rows: [],
};
shopGroup.dos.push(doGroup);
}
doGroup.rows.push(row);
}

return Array.from(shopMap.values())
.sort((a, b) => a.shopKey.localeCompare(b.shopKey, undefined, { numeric: true }))
.map((shopGroup) => ({
...shopGroup,
dos: shopGroup.dos
.sort((a, b) =>
a.sourceDoCode.localeCompare(b.sourceDoCode, undefined, { numeric: true }),
)
.map((doGroup) => ({
...doGroup,
rows: [...doGroup.rows],
})),
}));
}

const DoReplenishmentTab: React.FC = () => {
const { t } = useTranslation("do");
const inFlightRef = useRef(false);
@@ -256,7 +316,6 @@ const DoReplenishmentTab: React.FC = () => {
status: detail.status,
lines: detail.deliveryOrderLines ?? [],
});
setDraftRows([]);
setSelectedLine(null);
setReplenishQtyInput("");
} catch {
@@ -334,6 +393,10 @@ const DoReplenishmentTab: React.FC = () => {
setDraftRows((prev) => prev.filter((r) => r.rowId !== rowId));
}, []);

const handleClearDraftRows = useCallback(() => {
setDraftRows([]);
}, []);

const handleSubmit = useCallback(async () => {
if (inFlightRef.current) return;
if (draftRows.length === 0) {
@@ -435,6 +498,11 @@ const DoReplenishmentTab: React.FC = () => {
);

const selectedLineUom = lineUomDisplay(selectedLine);
const sourceTruckLaneDisplay = sourceDo
? sourceDo.truckLaneCode?.trim()
? sourceDo.truckLaneCode
: t("Truck X")
: "";

const datePickerSlotProps = useMemo(
() => ({
@@ -450,33 +518,256 @@ const DoReplenishmentTab: React.FC = () => {
[t],
);

const groupedDraftRows = useMemo(
() => groupDraftRowsByShopAndDo(draftRows),
[draftRows],
);

const currentDoDraftRows = useMemo(
() =>
sourceDo
? draftRows.filter((row) => row.sourceDoId === sourceDo.doId)
: [],
[draftRows, sourceDo],
);

const draftPreviewPanel = (
<Paper
variant="outlined"
sx={{
p: 2,
height: "100%",
display: "flex",
flexDirection: "column",
bgcolor: (theme) => (theme.palette.mode === "dark" ? "grey.900" : "common.white"),
}}
>
<Stack spacing={0.5} sx={{ mb: 1.5 }}>
<Typography variant="subtitle2" fontWeight={700}>
{t("Draft List")}
{draftRows.length > 0 ? ` (${draftRows.length})` : ""}
</Typography>
<Typography variant="caption" color="text.secondary">
{t("Replenishment preview hint")}
</Typography>
</Stack>

{draftRows.length === 0 ? (
<Box
sx={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: 120,
border: (theme) => `1px dashed ${theme.palette.divider}`,
borderRadius: 2,
px: 2,
}}
>
<Typography variant="body2" color="text.secondary" textAlign="center">
{t("Replenishment preview empty")}
</Typography>
</Box>
) : (
<Stack
spacing={1.5}
sx={{
flex: 1,
minHeight: 0,
maxHeight: { xs: 360, lg: "calc(100vh - 280px)" },
overflowY: "auto",
overflowX: "hidden",
pr: 0.25,
pb: 1.5,
"& > *": { flexShrink: 0 },
}}
>
{groupedDraftRows.map((shopGroup) => {
const shopDisplay =
shopGroup.shopCode || shopGroup.shopName?.trim() || shopGroup.shopKey;
return (
<Paper
key={shopGroup.shopKey}
variant="outlined"
sx={{
p: 1.5,
flexShrink: 0,
overflow: "visible",
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "grey.800" : "grey.50",
}}
>
<Typography variant="caption" color="text.secondary" display="block">
{t("Shop Code")}
</Typography>
<Tooltip
title={shopGroup.shopName?.trim() ? shopGroup.shopName : ""}
placement="top"
arrow
disableHoverListener={!shopGroup.shopName?.trim()}
>
<Typography
variant="subtitle2"
fontWeight={700}
sx={{ wordBreak: "break-all", lineHeight: 1.4, mb: 1 }}
>
{shopDisplay}
</Typography>
</Tooltip>

<Stack spacing={1}>
{shopGroup.dos.map((doGroup) => (
<Box
key={`${shopGroup.shopKey}-${doGroup.sourceDoId}`}
sx={(theme) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1.5,
p: 1.25,
bgcolor:
theme.palette.mode === "dark" ? "grey.900" : "common.white",
})}
>
<Typography variant="caption" color="text.secondary" display="block">
{t("Delivery Order Code")}
</Typography>
<Tooltip title={doGroup.sourceDoCode} placement="top" arrow>
<Typography
variant="body2"
fontWeight={600}
sx={{ wordBreak: "break-all", lineHeight: 1.4, mb: 1 }}
>
{doGroup.sourceDoCode}
</Typography>
</Tooltip>

<Stack spacing={0.75}>
{doGroup.rows.map((row) => {
const qtyLabel = row.shortUom
? `${row.replenishQty} ${row.shortUom}`
: String(row.replenishQty);
return (
<Box
key={row.rowId}
sx={(theme) => ({
position: "relative",
pr: 4,
py: 0.75,
px: 1,
borderRadius: 1,
bgcolor:
theme.palette.mode === "dark"
? "grey.800"
: "grey.50",
})}
>
<IconButton
size="small"
color="error"
onClick={() => handleRemoveDraftRow(row.rowId)}
aria-label={t("Delete")}
sx={{ position: "absolute", top: 2, right: 2 }}
>
<Delete fontSize="small" />
</IconButton>

<Stack
direction="row"
spacing={1}
alignItems="flex-start"
sx={{ mb: 0.25 }}
>
<Typography
variant="body2"
fontWeight={600}
sx={{ flexShrink: 0, whiteSpace: "nowrap" }}
>
{row.itemNo}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
minWidth: 0,
flex: 1,
lineHeight: 1.4,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{row.itemName}
</Typography>
</Stack>

<Typography variant="body2">
<Box component="span" color="text.secondary">
{t("Replenish Qty")}:{" "}
</Box>
{qtyLabel}
</Typography>
</Box>
);
})}
</Stack>
</Box>
))}
</Stack>
</Paper>
);
})}
</Stack>
)}

{draftRows.length > 0 && (
<Stack direction="row" spacing={1} justifyContent="flex-end" sx={{ mt: 1.5 }}>
<Button variant="outlined" color="inherit" onClick={handleClearDraftRows}>
{t("Clear")}
</Button>
<Button variant="contained" onClick={() => void handleSubmit()} disabled={isSubmitting}>
{t("Submit")}
</Button>
</Stack>
)}
</Paper>
);

return (
<Stack spacing={2}>
<Paper
variant="outlined"
<Box
sx={{
position: "relative",
p: 2,
bgcolor: (theme) => (theme.palette.mode === "dark" ? "grey.900" : "grey.50"),
display: "grid",
gridTemplateColumns: { xs: "1fr", lg: "minmax(0, 1fr) minmax(340px, 420px)" },
gap: 2,
alignItems: "stretch",
}}
>
<Tooltip title={t("Replenishment Tracking")}>
<IconButton
size="small"
onClick={() => setTrackingDialogOpen(true)}
aria-label={t("Replenishment Tracking")}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 1,
color: "text.secondary",
}}
>
<InfoOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
<Stack spacing={2}>
<Paper
variant="outlined"
sx={{
position: "relative",
p: 2,
bgcolor: (theme) => (theme.palette.mode === "dark" ? "grey.900" : "grey.50"),
}}
>
<Tooltip title={t("Replenishment Tracking")}>
<IconButton
size="small"
onClick={() => setTrackingDialogOpen(true)}
aria-label={t("Replenishment Tracking")}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 1,
color: "text.secondary",
}}
>
<InfoOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
<Stack spacing={2}>
<Box
sx={{
display: "grid",
@@ -598,34 +889,82 @@ const DoReplenishmentTab: React.FC = () => {
<TableCell sx={{ width: { md: "18%" }, minWidth: { md: 168 } }}>
{t("Replenishment item code")}
</TableCell>
<TableCell sx={{ width: { md: "28%" } }}>{t("Item Name")}</TableCell>
<TableCell align="right" sx={{ width: { md: "11%" }, whiteSpace: "nowrap" }}>
<TableCell sx={{ width: { md: "24%" } }}>{t("Item Name")}</TableCell>
<TableCell align="right" sx={{ width: { md: "10%" }, whiteSpace: "nowrap" }}>
{t("Original Shipment Qty")}
</TableCell>
<TableCell align="right" sx={{ width: { md: "12%" }, whiteSpace: "nowrap" }}>
<TableCell align="right" sx={{ width: { md: "10%" }, whiteSpace: "nowrap" }}>
{t("Replenish Qty")}
</TableCell>
<TableCell sx={{ width: { md: "8%" }, minWidth: { md: 48 }, whiteSpace: "nowrap" }}>
<TableCell sx={{ width: { md: "7%" }, minWidth: { md: 48 }, whiteSpace: "nowrap" }}>
{t("uom")}
</TableCell>
<TableCell sx={{ width: { md: "10%" }, minWidth: { md: 88 }, whiteSpace: "nowrap" }}>
<TableCell
sx={{
width: { md: "15%" },
minWidth: { md: 96 },
maxWidth: 0,
overflow: "hidden",
}}
>
{t("Truck Lance Code")}
</TableCell>
<TableCell align="center" sx={{ width: { md: 120 }, whiteSpace: "nowrap" }}>
<TableCell
align="center"
sx={{ width: { md: 108 }, minWidth: 108, whiteSpace: "nowrap" }}
>
{t("Action")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{draftRows.map((row) => (
<TableRow key={row.rowId} hover>
{currentDoDraftRows.map((row) => (
<TableRow
key={row.rowId}
hover
sx={(theme) => ({
bgcolor:
theme.palette.mode === "dark"
? "action.selected"
: "action.hover",
})}
>
<TableCell>{row.itemNo}</TableCell>
<TableCell sx={{ wordBreak: "break-word" }}>{row.itemName}</TableCell>
<TableCell align="right">{row.originalQty}</TableCell>
<TableCell align="right">{row.replenishQty}</TableCell>
<TableCell>{row.shortUom || "—"}</TableCell>
<TableCell>
{row.truckLaneCode?.trim() || sourceDo.truckLaneCode?.trim() || t("Truck X")}
<TableCell
sx={{
maxWidth: 0,
overflow: "hidden",
verticalAlign: "middle",
}}
>
<Tooltip
title={
row.truckLaneCode?.trim() ||
sourceDo.truckLaneCode?.trim() ||
t("Truck X")
}
placement="top"
arrow
>
<Box
component="span"
sx={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
}}
>
{row.truckLaneCode?.trim() ||
sourceDo.truckLaneCode?.trim() ||
t("Truck X")}
</Box>
</Tooltip>
</TableCell>
<TableCell align="center">
<Box sx={REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX}>
@@ -724,16 +1063,29 @@ const DoReplenishmentTab: React.FC = () => {
sx={{ whiteSpace: "nowrap" }}
/>
</TableCell>
<TableCell>
<ReplenishmentItemEntryPlainText
reserveSpace
value={
sourceDo.truckLaneCode?.trim()
? sourceDo.truckLaneCode
: t("Truck X")
}
sx={{ whiteSpace: "nowrap" }}
/>
<TableCell
sx={{
maxWidth: 0,
overflow: "hidden",
verticalAlign: "middle",
}}
>
<Tooltip title={sourceTruckLaneDisplay} placement="top" arrow>
<Box
component="span"
sx={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
minHeight: (theme) => theme.spacing(5),
lineHeight: (theme) => theme.spacing(5),
}}
>
{sourceTruckLaneDisplay}
</Box>
</Tooltip>
</TableCell>
<TableCell align="center">
<Box sx={REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX}>
@@ -758,22 +1110,17 @@ const DoReplenishmentTab: React.FC = () => {
</TableBody>
</Table>
</TableContainer>

{draftRows.length > 0 && (
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
variant="contained"
onClick={() => void handleSubmit()}
disabled={isSubmitting}
>
{t("Submit")}
</Button>
</Box>
)}
</Stack>
)}
</Stack>
</Paper>
</Stack>
</Paper>

<Box sx={{ display: { xs: "none", lg: "block" }, minHeight: 0 }}>
{draftPreviewPanel}
</Box>
</Box>

<Box sx={{ display: { xs: "block", lg: "none" } }}>{draftPreviewPanel}</Box>

<Dialog
open={trackingDialogOpen}


+ 3
- 0
src/i18n/en/do.json Целия файл

@@ -75,6 +75,9 @@
"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.",
"Clear": "Clear",
"Enter item code to search": "Enter item code to search",
"Failed to lookup source DO": "Failed to lookup source DO",
"Item": "Item",


+ 3
- 0
src/i18n/zh/do.json Целия файл

@@ -23,6 +23,9 @@
"Please verify DO code suffix, delivery date and shop.": "請核對送貨單號末四位、送貨日及店鋪資料。",
"Shop code or name is required": "請輸入店鋪代碼或名稱",
"Draft List": "待提交列表",
"Replenishment preview hint": "可從不同來源送貨單加入品項,在此批次提交。",
"Replenishment preview empty": "加入的品項會顯示於此;可再查詢其他來源送貨單繼續加入。",
"Clear": "清空",
"Failed to lookup source DO": "查詢來源送貨單失敗",
"Item": "物品",
"Lookup": "查詢",


Зареждане…
Отказ
Запис