|
|
|
@@ -0,0 +1,785 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import React, { useCallback, useMemo, useRef, useState } from "react"; |
|
|
|
import { |
|
|
|
Autocomplete, |
|
|
|
Box, |
|
|
|
Button, |
|
|
|
Dialog, |
|
|
|
DialogContent, |
|
|
|
DialogTitle, |
|
|
|
FormControl, |
|
|
|
IconButton, |
|
|
|
InputLabel, |
|
|
|
MenuItem, |
|
|
|
Paper, |
|
|
|
Select, |
|
|
|
Stack, |
|
|
|
Table, |
|
|
|
TableBody, |
|
|
|
TableCell, |
|
|
|
TableContainer, |
|
|
|
TableHead, |
|
|
|
TableRow, |
|
|
|
Tooltip, |
|
|
|
Typography, |
|
|
|
} from "@mui/material"; |
|
|
|
import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; |
|
|
|
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 { 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 { GridColDef } from "@mui/x-data-grid"; |
|
|
|
import Swal from "sweetalert2"; |
|
|
|
import StyledDataGrid from "../StyledDataGrid"; |
|
|
|
import { DoDetail, DoDetailLine, fetchDoDetail, fetchDoSearch } from "@/app/api/do/actions"; |
|
|
|
import { arrayToDateString } from "@/app/utils/formatUtil"; |
|
|
|
import { |
|
|
|
REPLENISHMENT_FIELD_ICON_SX, |
|
|
|
REPLENISHMENT_TABLE_AUTOCOMPLETE_SX, |
|
|
|
REPLENISHMENT_TABLE_ENTRY_ROW_SX, |
|
|
|
REPLENISHMENT_TABLE_INLINE_TEXTFIELD_SX, |
|
|
|
REPLENISHMENT_TABLE_SX, |
|
|
|
REPLENISHMENT_LOOKUP_BUTTON_SX, |
|
|
|
REPLENISHMENT_SOURCE_HEADER_SX, |
|
|
|
REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX, |
|
|
|
REPLENISHMENT_TEXTFIELD_SX, |
|
|
|
ReplenishmentFieldLabel, |
|
|
|
ReplenishmentItemEntryPlainText, |
|
|
|
ReplenishmentTextField, |
|
|
|
replenishmentSearchGridInputSx, |
|
|
|
replenishmentSearchGridLabelSx, |
|
|
|
replenishmentSearchGridShopRowSx, |
|
|
|
} from "./ReplenishmentFilterField"; |
|
|
|
|
|
|
|
export type ReplenishmentStatus = "pending" | "processing" | "completed"; |
|
|
|
|
|
|
|
export type ReplenishmentDraftRow = { |
|
|
|
rowId: string; |
|
|
|
deliveryDate: string; |
|
|
|
sourceDoId: number; |
|
|
|
sourceDoCode: string; |
|
|
|
sourceDoLineId: number; |
|
|
|
itemId?: number; |
|
|
|
itemNo: string; |
|
|
|
itemName: string; |
|
|
|
originalQty: number; |
|
|
|
replenishQty: number; |
|
|
|
shortUom?: string; |
|
|
|
shopCode?: string; |
|
|
|
shopName?: string; |
|
|
|
truckLaneCode?: string; |
|
|
|
}; |
|
|
|
|
|
|
|
export type ReplenishmentRecord = ReplenishmentDraftRow & { |
|
|
|
id: number; |
|
|
|
code: string; |
|
|
|
targetDoId?: number; |
|
|
|
targetDoCode?: string; |
|
|
|
pickOrderLineId?: number; |
|
|
|
status: ReplenishmentStatus; |
|
|
|
created: string; |
|
|
|
}; |
|
|
|
|
|
|
|
type SourceDoContext = { |
|
|
|
doId: number; |
|
|
|
doCode: string; |
|
|
|
shopCode?: string; |
|
|
|
shopName?: string; |
|
|
|
truckLaneCode?: string | null; |
|
|
|
status: string; |
|
|
|
lines: DoDetailLine[]; |
|
|
|
}; |
|
|
|
|
|
|
|
let localIdSeq = 1; |
|
|
|
let replenishmentCodeSeq = 1; |
|
|
|
|
|
|
|
function nextReplenishmentCode(deliveryDate: string): string { |
|
|
|
const ymd = deliveryDate.replace(/-/g, ""); |
|
|
|
const seq = String(replenishmentCodeSeq++).padStart(3, "0"); |
|
|
|
return `RP-${ymd}-${seq}`; |
|
|
|
} |
|
|
|
|
|
|
|
/** Shop code: partial match. Shop name: prefix match (e.g. first 4 characters). */ |
|
|
|
function matchesShopInput(detail: DoDetail, shopInput: string): boolean { |
|
|
|
const normalized = shopInput.trim().toLowerCase(); |
|
|
|
if (!normalized) return false; |
|
|
|
const code = detail.shopCode?.toLowerCase() ?? ""; |
|
|
|
const name = detail.shopName?.toLowerCase() ?? ""; |
|
|
|
return code.includes(normalized) || name.startsWith(normalized); |
|
|
|
} |
|
|
|
|
|
|
|
function matchesDeliveryDate( |
|
|
|
estimatedArrivalDate: number[] | undefined, |
|
|
|
deliveryDateStr: string, |
|
|
|
): boolean { |
|
|
|
if (!estimatedArrivalDate?.length) return false; |
|
|
|
return arrayToDateString(estimatedArrivalDate) === deliveryDateStr; |
|
|
|
} |
|
|
|
|
|
|
|
function lineUomDisplay(line?: DoDetailLine | null): string { |
|
|
|
if (!line) return ""; |
|
|
|
return (line.shortUom ?? line.uomCode ?? line.uom ?? "").trim(); |
|
|
|
} |
|
|
|
|
|
|
|
const DoReplenishmentTab: React.FC = () => { |
|
|
|
const { t } = useTranslation("do"); |
|
|
|
const inFlightRef = useRef(false); |
|
|
|
const itemCodeInputRef = useRef<HTMLInputElement>(null); |
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false); |
|
|
|
const [isLookingUp, setIsLookingUp] = useState(false); |
|
|
|
|
|
|
|
const [deliveryDate, setDeliveryDate] = useState<Dayjs | null>(dayjs()); |
|
|
|
const [doCodeSuffix, setDoCodeSuffix] = useState(""); |
|
|
|
const [shopInput, setShopInput] = useState(""); |
|
|
|
const [sourceDo, setSourceDo] = useState<SourceDoContext | null>(null); |
|
|
|
const [selectedLine, setSelectedLine] = useState<DoDetailLine | null>(null); |
|
|
|
const [replenishQtyInput, setReplenishQtyInput] = useState(""); |
|
|
|
|
|
|
|
const [draftRows, setDraftRows] = useState<ReplenishmentDraftRow[]>([]); |
|
|
|
const [records, setRecords] = useState<ReplenishmentRecord[]>([]); |
|
|
|
const [trackStatusFilter, setTrackStatusFilter] = useState<ReplenishmentStatus | "all">("all"); |
|
|
|
const [trackDateFilter, setTrackDateFilter] = useState<Dayjs | null>(null); |
|
|
|
const [trackingDialogOpen, setTrackingDialogOpen] = useState(false); |
|
|
|
|
|
|
|
const deliveryDateStr = deliveryDate?.format("YYYY-MM-DD") ?? ""; |
|
|
|
|
|
|
|
const handleLookupSourceDo = useCallback(async () => { |
|
|
|
const suffix = doCodeSuffix.trim(); |
|
|
|
const shop = shopInput.trim(); |
|
|
|
if (suffix.length !== 4) { |
|
|
|
await Swal.fire({ |
|
|
|
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") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!shop) { |
|
|
|
await Swal.fire({ icon: "warning", title: t("Shop code or name is required") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
setIsLookingUp(true); |
|
|
|
try { |
|
|
|
const searchRes = await fetchDoSearch( |
|
|
|
suffix, |
|
|
|
"", |
|
|
|
"completed", |
|
|
|
"", |
|
|
|
"", |
|
|
|
`${deliveryDateStr}T00:00:00`, |
|
|
|
"", |
|
|
|
1, |
|
|
|
100, |
|
|
|
undefined, |
|
|
|
null, |
|
|
|
null, |
|
|
|
); |
|
|
|
const candidates = searchRes.records.filter( |
|
|
|
(r) => |
|
|
|
r.code.endsWith(suffix) && |
|
|
|
matchesDeliveryDate(r.estimatedArrivalDate, deliveryDateStr), |
|
|
|
); |
|
|
|
if (candidates.length === 0) { |
|
|
|
await Swal.fire({ 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") }); |
|
|
|
setSourceDo(null); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (matched.length > 1) { |
|
|
|
await Swal.fire({ |
|
|
|
icon: "error", |
|
|
|
title: t("Multiple source DOs matched"), |
|
|
|
text: t("Please verify DO code suffix, delivery date and shop."), |
|
|
|
}); |
|
|
|
setSourceDo(null); |
|
|
|
return; |
|
|
|
} |
|
|
|
const detail = matched[0]; |
|
|
|
const matchedCandidate = candidates.find((c) => c.id === detail.id); |
|
|
|
if (detail.status !== "completed") { |
|
|
|
await Swal.fire({ |
|
|
|
icon: "error", |
|
|
|
title: t("Source DO must be completed"), |
|
|
|
text: t("Only completed delivery orders can be used as replenishment source."), |
|
|
|
}); |
|
|
|
setSourceDo(null); |
|
|
|
return; |
|
|
|
} |
|
|
|
setSourceDo({ |
|
|
|
doId: detail.id, |
|
|
|
doCode: detail.code, |
|
|
|
shopCode: detail.shopCode, |
|
|
|
shopName: detail.shopName, |
|
|
|
truckLaneCode: matchedCandidate?.truckLanceCode ?? null, |
|
|
|
status: detail.status, |
|
|
|
lines: detail.deliveryOrderLines ?? [], |
|
|
|
}); |
|
|
|
setDraftRows([]); |
|
|
|
setSelectedLine(null); |
|
|
|
setReplenishQtyInput(""); |
|
|
|
} catch { |
|
|
|
await Swal.fire({ icon: "error", title: t("Failed to lookup source DO") }); |
|
|
|
} finally { |
|
|
|
setIsLookingUp(false); |
|
|
|
} |
|
|
|
}, [deliveryDateStr, doCodeSuffix, shopInput, t]); |
|
|
|
|
|
|
|
const handleAddDraftRow = useCallback(() => { |
|
|
|
if (!sourceDo) { |
|
|
|
void Swal.fire({ icon: "warning", title: t("Please lookup source DO first") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!deliveryDateStr) { |
|
|
|
void Swal.fire({ icon: "warning", title: t("Delivery date is required") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!selectedLine) { |
|
|
|
void Swal.fire({ 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") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
const line = selectedLine; |
|
|
|
|
|
|
|
const duplicate = draftRows.some( |
|
|
|
(r) => r.sourceDoLineId === line.id && r.sourceDoId === sourceDo.doId, |
|
|
|
); |
|
|
|
if (duplicate) { |
|
|
|
void Swal.fire({ icon: "warning", title: t("This item is already in the draft list") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
setDraftRows((prev) => [ |
|
|
|
...prev, |
|
|
|
{ |
|
|
|
rowId: `draft-${Date.now()}-${prev.length}`, |
|
|
|
deliveryDate: deliveryDateStr, |
|
|
|
sourceDoId: sourceDo.doId, |
|
|
|
sourceDoCode: sourceDo.doCode, |
|
|
|
sourceDoLineId: line.id, |
|
|
|
itemNo: line.itemNo ?? "", |
|
|
|
itemName: line.itemName ?? line.itemNo ?? "", |
|
|
|
originalQty: line.qty ?? 0, |
|
|
|
replenishQty: qty, |
|
|
|
shortUom: lineUomDisplay(line) || undefined, |
|
|
|
shopCode: sourceDo.shopCode, |
|
|
|
shopName: sourceDo.shopName, |
|
|
|
truckLaneCode: undefined, |
|
|
|
}, |
|
|
|
]); |
|
|
|
setSelectedLine(null); |
|
|
|
setReplenishQtyInput(""); |
|
|
|
window.setTimeout(() => itemCodeInputRef.current?.focus(), 0); |
|
|
|
}, [ |
|
|
|
deliveryDateStr, |
|
|
|
draftRows, |
|
|
|
replenishQtyInput, |
|
|
|
selectedLine, |
|
|
|
sourceDo, |
|
|
|
t, |
|
|
|
]); |
|
|
|
|
|
|
|
const handleRemoveDraftRow = useCallback((rowId: string) => { |
|
|
|
setDraftRows((prev) => prev.filter((r) => r.rowId !== rowId)); |
|
|
|
}, []); |
|
|
|
|
|
|
|
const handleSubmit = useCallback(async () => { |
|
|
|
if (inFlightRef.current) return; |
|
|
|
if (draftRows.length === 0) { |
|
|
|
await Swal.fire({ icon: "warning", title: t("No draft rows to submit") }); |
|
|
|
return; |
|
|
|
} |
|
|
|
inFlightRef.current = true; |
|
|
|
setIsSubmitting(true); |
|
|
|
try { |
|
|
|
const now = new Date().toISOString(); |
|
|
|
const newRecords: ReplenishmentRecord[] = draftRows.map((row) => ({ |
|
|
|
...row, |
|
|
|
id: localIdSeq++, |
|
|
|
code: nextReplenishmentCode(row.deliveryDate), |
|
|
|
status: "pending", |
|
|
|
created: now, |
|
|
|
})); |
|
|
|
setRecords((prev) => [...newRecords, ...prev]); |
|
|
|
setDraftRows([]); |
|
|
|
await Swal.fire({ |
|
|
|
icon: "info", |
|
|
|
title: t("Replenishment API not ready"), |
|
|
|
text: t("Records saved locally for preview. Backend integration pending."), |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setIsSubmitting(false); |
|
|
|
inFlightRef.current = false; |
|
|
|
} |
|
|
|
}, [draftRows, t]); |
|
|
|
|
|
|
|
const trackColumns: GridColDef<ReplenishmentRecord>[] = useMemo( |
|
|
|
() => [ |
|
|
|
{ field: "code", headerName: t("Replenishment Code"), width: 140 }, |
|
|
|
{ field: "sourceDoCode", headerName: t("Source DO"), width: 120 }, |
|
|
|
{ field: "shopName", headerName: t("Shop Name"), flex: 1, minWidth: 120 }, |
|
|
|
{ |
|
|
|
field: "truckLaneCode", |
|
|
|
headerName: t("Truck Lance Code"), |
|
|
|
width: 120, |
|
|
|
valueGetter: (params) => params.row.truckLaneCode ?? "—", |
|
|
|
}, |
|
|
|
{ field: "itemNo", headerName: t("Item No."), width: 100 }, |
|
|
|
{ field: "itemName", headerName: t("Item Name"), flex: 1, minWidth: 120 }, |
|
|
|
{ |
|
|
|
field: "replenishQty", |
|
|
|
headerName: t("Replenish Qty"), |
|
|
|
width: 120, |
|
|
|
valueGetter: (params) => { |
|
|
|
const row = params.row as ReplenishmentRecord; |
|
|
|
return row.shortUom ? `${row.replenishQty} ${row.shortUom}` : row.replenishQty; |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
field: "targetDoCode", |
|
|
|
headerName: t("Target DO"), |
|
|
|
width: 120, |
|
|
|
valueGetter: (params) => params.row.targetDoCode ?? "—", |
|
|
|
}, |
|
|
|
{ |
|
|
|
field: "status", |
|
|
|
headerName: t("Status"), |
|
|
|
width: 110, |
|
|
|
valueFormatter: (params) => t(String(params.value)), |
|
|
|
}, |
|
|
|
{ |
|
|
|
field: "created", |
|
|
|
headerName: t("Created"), |
|
|
|
width: 160, |
|
|
|
valueFormatter: (params) => |
|
|
|
params.value ? dayjs(String(params.value)).format("YYYY-MM-DD HH:mm") : "", |
|
|
|
}, |
|
|
|
], |
|
|
|
[t], |
|
|
|
); |
|
|
|
|
|
|
|
const selectedLineUom = lineUomDisplay(selectedLine); |
|
|
|
|
|
|
|
const filteredRecords = useMemo(() => { |
|
|
|
return records.filter((r) => { |
|
|
|
if (trackStatusFilter !== "all" && r.status !== trackStatusFilter) return false; |
|
|
|
if (trackDateFilter && r.deliveryDate !== trackDateFilter.format("YYYY-MM-DD")) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}); |
|
|
|
}, [records, trackDateFilter, trackStatusFilter]); |
|
|
|
|
|
|
|
const datePickerSlotProps = useMemo( |
|
|
|
() => ({ |
|
|
|
textField: { |
|
|
|
size: "small" as const, |
|
|
|
fullWidth: true, |
|
|
|
variant: "filled" as const, |
|
|
|
placeholder: t("replenishmentDatePlaceholder"), |
|
|
|
sx: REPLENISHMENT_TEXTFIELD_SX, |
|
|
|
InputProps: { disableUnderline: true }, |
|
|
|
}, |
|
|
|
}), |
|
|
|
[t], |
|
|
|
); |
|
|
|
|
|
|
|
return ( |
|
|
|
<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", |
|
|
|
gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr 1fr" }, |
|
|
|
columnGap: 2, |
|
|
|
rowGap: 1, |
|
|
|
alignItems: "stretch", |
|
|
|
pr: { xs: 4, lg: 4 }, |
|
|
|
}} |
|
|
|
> |
|
|
|
<ReplenishmentFieldLabel |
|
|
|
icon={<CalendarTodayIcon fontSize="small" sx={REPLENISHMENT_FIELD_ICON_SX} />} |
|
|
|
title={t("Estimated Arrival Date")} |
|
|
|
required |
|
|
|
sx={replenishmentSearchGridLabelSx(1)} |
|
|
|
/> |
|
|
|
<Box sx={replenishmentSearchGridInputSx(1)}> |
|
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}> |
|
|
|
<DatePicker |
|
|
|
format="YYYY-MM-DD" |
|
|
|
value={deliveryDate} |
|
|
|
onChange={(v) => { |
|
|
|
setDeliveryDate(v); |
|
|
|
setSourceDo(null); |
|
|
|
}} |
|
|
|
slotProps={datePickerSlotProps} |
|
|
|
sx={{ width: "100%" }} |
|
|
|
/> |
|
|
|
</LocalizationProvider> |
|
|
|
</Box> |
|
|
|
|
|
|
|
<ReplenishmentFieldLabel |
|
|
|
icon={<ReceiptLongIcon fontSize="small" sx={REPLENISHMENT_FIELD_ICON_SX} />} |
|
|
|
title={t("DO Code Last 4")} |
|
|
|
required |
|
|
|
sx={replenishmentSearchGridLabelSx(2)} |
|
|
|
/> |
|
|
|
<Box sx={replenishmentSearchGridInputSx(2)}> |
|
|
|
<ReplenishmentTextField |
|
|
|
value={doCodeSuffix} |
|
|
|
onChange={(e) => { |
|
|
|
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(); |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
</Box> |
|
|
|
|
|
|
|
<ReplenishmentFieldLabel |
|
|
|
icon={<StorefrontIcon fontSize="small" sx={REPLENISHMENT_FIELD_ICON_SX} />} |
|
|
|
title={t("Shop Code")} |
|
|
|
required |
|
|
|
sx={replenishmentSearchGridLabelSx(3)} |
|
|
|
/> |
|
|
|
<Box sx={replenishmentSearchGridShopRowSx}> |
|
|
|
<ReplenishmentTextField |
|
|
|
value={shopInput} |
|
|
|
onChange={(e) => { |
|
|
|
setShopInput(e.target.value); |
|
|
|
setSourceDo(null); |
|
|
|
}} |
|
|
|
placeholder={t("replenishmentShopPlaceholder")} |
|
|
|
onKeyDown={(e) => { |
|
|
|
if (e.key === "Enter") { |
|
|
|
e.preventDefault(); |
|
|
|
void handleLookupSourceDo(); |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
disableElevation |
|
|
|
startIcon={<Search fontSize="small" />} |
|
|
|
onClick={() => void handleLookupSourceDo()} |
|
|
|
disabled={isLookingUp} |
|
|
|
sx={REPLENISHMENT_LOOKUP_BUTTON_SX} |
|
|
|
> |
|
|
|
{t("Lookup")} |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
|
|
|
|
{sourceDo && ( |
|
|
|
<Box sx={REPLENISHMENT_SOURCE_HEADER_SX}> |
|
|
|
<Typography |
|
|
|
variant="body2" |
|
|
|
fontWeight={700} |
|
|
|
sx={{ wordBreak: "break-word", lineHeight: 1.5, width: "100%" }} |
|
|
|
> |
|
|
|
{t("Delivery Order Code")}: {sourceDo.doCode} |
|
|
|
{" "} |
|
|
|
{t("Shop Name")}: {sourceDo.shopName ?? "—"} |
|
|
|
{" "} |
|
|
|
{t("Truck Lance Code")}:{" "} |
|
|
|
{sourceDo.truckLaneCode?.trim() ? sourceDo.truckLaneCode : t("Truck X")} |
|
|
|
</Typography> |
|
|
|
</Box> |
|
|
|
)} |
|
|
|
|
|
|
|
{sourceDo && ( |
|
|
|
<Stack spacing={1.5}> |
|
|
|
<TableContainer |
|
|
|
sx={(theme) => ({ |
|
|
|
border: `1px solid ${theme.palette.divider}`, |
|
|
|
borderRadius: 2, |
|
|
|
bgcolor: theme.palette.mode === "dark" ? "grey.900" : "common.white", |
|
|
|
})} |
|
|
|
> |
|
|
|
<Table size="small" sx={REPLENISHMENT_TABLE_SX}> |
|
|
|
<TableHead> |
|
|
|
<TableRow> |
|
|
|
<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" }}> |
|
|
|
{t("Original Shipment Qty")} |
|
|
|
</TableCell> |
|
|
|
<TableCell align="right" sx={{ width: { md: "12%" }, whiteSpace: "nowrap" }}> |
|
|
|
{t("Replenish Qty")} |
|
|
|
</TableCell> |
|
|
|
<TableCell sx={{ width: { md: "8%" }, minWidth: { md: 48 }, whiteSpace: "nowrap" }}> |
|
|
|
{t("uom")} |
|
|
|
</TableCell> |
|
|
|
<TableCell align="center" sx={{ width: { md: 120 }, whiteSpace: "nowrap" }}> |
|
|
|
{t("Action")} |
|
|
|
</TableCell> |
|
|
|
</TableRow> |
|
|
|
</TableHead> |
|
|
|
<TableBody> |
|
|
|
{draftRows.map((row) => ( |
|
|
|
<TableRow key={row.rowId} 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 align="center"> |
|
|
|
<Box sx={REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX}> |
|
|
|
<IconButton |
|
|
|
size="small" |
|
|
|
color="error" |
|
|
|
onClick={() => handleRemoveDraftRow(row.rowId)} |
|
|
|
aria-label={t("Delete")} |
|
|
|
> |
|
|
|
<Delete fontSize="small" /> |
|
|
|
</IconButton> |
|
|
|
</Box> |
|
|
|
</TableCell> |
|
|
|
</TableRow> |
|
|
|
))} |
|
|
|
<TableRow sx={REPLENISHMENT_TABLE_ENTRY_ROW_SX}> |
|
|
|
<TableCell> |
|
|
|
<Autocomplete |
|
|
|
size="small" |
|
|
|
fullWidth |
|
|
|
options={sourceDo.lines.filter( |
|
|
|
(line) => |
|
|
|
!draftRows.some( |
|
|
|
(r) => |
|
|
|
r.sourceDoLineId === line.id && r.sourceDoId === sourceDo.doId, |
|
|
|
), |
|
|
|
)} |
|
|
|
value={selectedLine} |
|
|
|
onChange={(_, newValue) => 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 ( |
|
|
|
<ReplenishmentTextField |
|
|
|
{...params} |
|
|
|
placeholder={t("Replenishment item code")} |
|
|
|
inputProps={{ |
|
|
|
...inputProps, |
|
|
|
ref: (node: HTMLInputElement | null) => { |
|
|
|
itemCodeInputRef.current = node; |
|
|
|
const { ref } = inputProps; |
|
|
|
if (typeof ref === "function") ref(node); |
|
|
|
else if (ref) { |
|
|
|
( |
|
|
|
ref as React.MutableRefObject<HTMLInputElement | null> |
|
|
|
).current = node; |
|
|
|
} |
|
|
|
}, |
|
|
|
}} |
|
|
|
InputProps={{ ...params.InputProps, disableUnderline: true }} |
|
|
|
/> |
|
|
|
); |
|
|
|
}} |
|
|
|
sx={REPLENISHMENT_TABLE_AUTOCOMPLETE_SX} |
|
|
|
/> |
|
|
|
</TableCell> |
|
|
|
<TableCell> |
|
|
|
<ReplenishmentItemEntryPlainText |
|
|
|
value={ |
|
|
|
selectedLine ? (selectedLine.itemName ?? selectedLine.itemNo ?? "") : "" |
|
|
|
} |
|
|
|
/> |
|
|
|
</TableCell> |
|
|
|
<TableCell align="right"> |
|
|
|
<ReplenishmentItemEntryPlainText |
|
|
|
reserveSpace |
|
|
|
value={selectedLine != null ? String(selectedLine.qty ?? "") : ""} |
|
|
|
sx={{ whiteSpace: "nowrap", textAlign: "right", minHeight: "unset" }} |
|
|
|
/> |
|
|
|
</TableCell> |
|
|
|
<TableCell align="right"> |
|
|
|
<Box sx={{ display: "flex", justifyContent: "flex-end" }}> |
|
|
|
<ReplenishmentTextField |
|
|
|
type="number" |
|
|
|
hiddenLabel |
|
|
|
fullWidth={false} |
|
|
|
value={replenishQtyInput} |
|
|
|
onChange={(e) => 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", |
|
|
|
}, |
|
|
|
})} |
|
|
|
/> |
|
|
|
</Box> |
|
|
|
</TableCell> |
|
|
|
<TableCell> |
|
|
|
<ReplenishmentItemEntryPlainText |
|
|
|
reserveSpace |
|
|
|
value={selectedLineUom} |
|
|
|
sx={{ whiteSpace: "nowrap" }} |
|
|
|
/> |
|
|
|
</TableCell> |
|
|
|
<TableCell align="center"> |
|
|
|
<Box sx={REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX}> |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
size="small" |
|
|
|
startIcon={<Add />} |
|
|
|
onClick={handleAddDraftRow} |
|
|
|
sx={{ |
|
|
|
borderRadius: 2, |
|
|
|
textTransform: "none", |
|
|
|
whiteSpace: "nowrap", |
|
|
|
px: 1.5, |
|
|
|
fontSize: (theme) => theme.typography.body2.fontSize, |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("Add Row")} |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
</TableCell> |
|
|
|
</TableRow> |
|
|
|
</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> |
|
|
|
|
|
|
|
<Dialog |
|
|
|
open={trackingDialogOpen} |
|
|
|
onClose={() => setTrackingDialogOpen(false)} |
|
|
|
maxWidth="lg" |
|
|
|
fullWidth |
|
|
|
> |
|
|
|
<DialogTitle |
|
|
|
sx={{ |
|
|
|
display: "flex", |
|
|
|
alignItems: "center", |
|
|
|
justifyContent: "space-between", |
|
|
|
pr: 1, |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("Replenishment Tracking")} |
|
|
|
<IconButton |
|
|
|
size="small" |
|
|
|
onClick={() => setTrackingDialogOpen(false)} |
|
|
|
aria-label={t("Cancel")} |
|
|
|
> |
|
|
|
<Close fontSize="small" /> |
|
|
|
</IconButton> |
|
|
|
</DialogTitle> |
|
|
|
<DialogContent dividers sx={{ p: 0 }}> |
|
|
|
<Box sx={{ px: 2, pt: 1.5, pb: 1 }}> |
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}> |
|
|
|
<Box sx={{ minWidth: 200, maxWidth: 280 }}> |
|
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}> |
|
|
|
<DatePicker |
|
|
|
format="YYYY-MM-DD" |
|
|
|
value={trackDateFilter} |
|
|
|
onChange={(v) => setTrackDateFilter(v)} |
|
|
|
slotProps={datePickerSlotProps} |
|
|
|
/> |
|
|
|
</LocalizationProvider> |
|
|
|
</Box> |
|
|
|
<FormControl size="small" sx={{ minWidth: 160 }}> |
|
|
|
<InputLabel>{t("Status")}</InputLabel> |
|
|
|
<Select |
|
|
|
label={t("Status")} |
|
|
|
value={trackStatusFilter} |
|
|
|
onChange={(e) => |
|
|
|
setTrackStatusFilter(e.target.value as ReplenishmentStatus | "all") |
|
|
|
} |
|
|
|
> |
|
|
|
<MenuItem value="all">{t("All")}</MenuItem> |
|
|
|
<MenuItem value="pending">{t("pending")}</MenuItem> |
|
|
|
<MenuItem value="processing">{t("processing")}</MenuItem> |
|
|
|
<MenuItem value="completed">{t("completed")}</MenuItem> |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
</Stack> |
|
|
|
</Box> |
|
|
|
<StyledDataGrid |
|
|
|
rows={filteredRecords} |
|
|
|
columns={trackColumns} |
|
|
|
autoHeight |
|
|
|
disableRowSelectionOnClick |
|
|
|
pageSizeOptions={[10, 25, 50]} |
|
|
|
initialState={{ pagination: { paginationModel: { pageSize: 10 } } }} |
|
|
|
/> |
|
|
|
</DialogContent> |
|
|
|
</Dialog> |
|
|
|
</Stack> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
export default DoReplenishmentTab; |