|
|
|
@@ -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} |
|
|
|
|