|
- "use client";
-
- import React, { useCallback, useEffect, 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,
- DoReplenishmentRecord,
- fetchDoDetail,
- fetchDoReplenishmentList,
- fetchDoSearch,
- submitDoReplenishment,
- } 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[];
- };
-
- function mapApiRecord(record: DoReplenishmentRecord): ReplenishmentRecord {
- return {
- rowId: `record-${record.id}`,
- deliveryDate: record.deliveryDate,
- sourceDoId: record.sourceDoId,
- sourceDoCode: record.sourceDoCode ?? "",
- sourceDoLineId: record.sourceDoLineId,
- itemId: record.itemId,
- itemNo: record.itemNo ?? "",
- itemName: record.itemName ?? "",
- originalQty: 0,
- replenishQty: Number(record.replenishQty),
- shortUom: record.shortUom,
- shopCode: record.shopCode,
- shopName: record.shopName,
- truckLaneCode: record.truckLaneCode,
- id: record.id,
- code: record.code,
- targetDoId: record.targetDoId,
- targetDoCode: record.targetDoCode,
- pickOrderLineId: record.pickOrderLineId,
- status: record.status as ReplenishmentStatus,
- created: record.created ?? "",
- };
- }
-
- /** 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();
- }
-
- 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);
- 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 [isLoadingTracking, setIsLoadingTracking] = useState(false);
- 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);
- const resolvedTruckLaneCode =
- detail.truckLaneCode?.trim() || matchedCandidate?.truckLanceCode?.trim() || null;
- 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: resolvedTruckLaneCode,
- status: detail.status,
- lines: detail.deliveryOrderLines ?? [],
- });
- 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 existingRowIndex = draftRows.findIndex(
- (r) => r.sourceDoLineId === line.id && r.sourceDoId === sourceDo.doId,
- );
-
- if (existingRowIndex >= 0) {
- setDraftRows((prev) =>
- prev.map((row, index) =>
- index === existingRowIndex
- ? { ...row, replenishQty: row.replenishQty + qty }
- : row,
- ),
- );
- } else {
- 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: sourceDo.truckLaneCode?.trim() || 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 handleClearDraftRows = useCallback(() => {
- setDraftRows([]);
- }, []);
-
- 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 created = await submitDoReplenishment(
- draftRows.map((row) => ({
- deliveryDate: row.deliveryDate,
- sourceDoId: row.sourceDoId,
- sourceDoLineId: row.sourceDoLineId,
- replenishQty: row.replenishQty,
- truckLaneCode: row.truckLaneCode,
- })),
- );
- setDraftRows([]);
- await Swal.fire({
- icon: "success",
- title: t("Replenishment submitted successfully"),
- text: created.map((row) => row.code).join(", "),
- });
- } catch (error: unknown) {
- const message =
- error instanceof Error ? error.message : t("Failed to submit replenishment");
- await Swal.fire({ icon: "error", title: message });
- } finally {
- setIsSubmitting(false);
- inFlightRef.current = false;
- }
- }, [draftRows, t]);
-
- const loadTrackingRecords = useCallback(async () => {
- setIsLoadingTracking(true);
- try {
- const data = await fetchDoReplenishmentList({
- deliveryDate: trackDateFilter?.format("YYYY-MM-DD"),
- status: trackStatusFilter,
- });
- setRecords(data.map(mapApiRecord));
- } catch {
- await Swal.fire({ icon: "error", title: t("Failed to load replenishment records") });
- } finally {
- setIsLoadingTracking(false);
- }
- }, [trackDateFilter, trackStatusFilter, t]);
-
- useEffect(() => {
- if (trackingDialogOpen) {
- void loadTrackingRecords();
- }
- }, [trackingDialogOpen, loadTrackingRecords]);
-
- 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 sourceTruckLaneDisplay = sourceDo
- ? sourceDo.truckLaneCode?.trim()
- ? sourceDo.truckLaneCode
- : t("Truck X")
- : "";
-
- 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],
- );
-
- 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}>
- <Box
- sx={{
- display: "grid",
- gridTemplateColumns: { xs: "1fr", lg: "minmax(0, 1fr) minmax(340px, 420px)" },
- gap: 2,
- alignItems: "stretch",
- }}
- >
- <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: "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: "10%" }, whiteSpace: "nowrap" }}>
- {t("Replenish Qty")}
- </TableCell>
- <TableCell sx={{ width: { md: "7%" }, minWidth: { md: 48 }, whiteSpace: "nowrap" }}>
- {t("uom")}
- </TableCell>
- <TableCell
- sx={{
- width: { md: "15%" },
- minWidth: { md: 96 },
- maxWidth: 0,
- overflow: "hidden",
- }}
- >
- {t("Truck Lance Code")}
- </TableCell>
- <TableCell
- align="center"
- sx={{ width: { md: 108 }, minWidth: 108, whiteSpace: "nowrap" }}
- >
- {t("Action")}
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {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
- 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}>
- <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}
- 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
- 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}>
- <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>
- </Stack>
- )}
- </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}
- 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={records}
- columns={trackColumns}
- autoHeight
- loading={isLoadingTracking}
- disableRowSelectionOnClick
- pageSizeOptions={[10, 25, 50]}
- initialState={{ pagination: { paginationModel: { pageSize: 10 } } }}
- />
- </DialogContent>
- </Dialog>
- </Stack>
- );
- };
-
- export default DoReplenishmentTab;
|