From a81fff570b78a1bff37b7f483f7dc6b72bddd9ac Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Fri, 12 Jun 2026 19:58:52 +0800 Subject: [PATCH] improvement replenishment --- .../DoSearch/DoReplenishmentTab.tsx | 266 +++++++++++------- .../DoSearch/ReplenishmentFilterField.tsx | 32 ++- src/i18n/en/do.json | 2 + src/i18n/zh/do.json | 2 + 4 files changed, 194 insertions(+), 108 deletions(-) diff --git a/src/components/DoSearch/DoReplenishmentTab.tsx b/src/components/DoSearch/DoReplenishmentTab.tsx index 17e014a..1df160d 100644 --- a/src/components/DoSearch/DoReplenishmentTab.tsx +++ b/src/components/DoSearch/DoReplenishmentTab.tsx @@ -10,7 +10,6 @@ import { DialogTitle, FormControl, IconButton, - InputLabel, MenuItem, Paper, Select, @@ -20,11 +19,13 @@ import { TableCell, TableContainer, TableHead, + TablePagination, TableRow, Tooltip, Typography, } from "@mui/material"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; +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"; @@ -33,9 +34,7 @@ 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, @@ -48,15 +47,18 @@ import { import { arrayToDateString } from "@/app/utils/formatUtil"; import { REPLENISHMENT_FIELD_ICON_SX, + REPLENISHMENT_FILLED_SELECT_SX, REPLENISHMENT_TABLE_AUTOCOMPLETE_SX, REPLENISHMENT_TABLE_ENTRY_ROW_SX, REPLENISHMENT_TABLE_INLINE_TEXTFIELD_SX, REPLENISHMENT_TABLE_SX, REPLENISHMENT_LOOKUP_BUTTON_SX, + REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX, REPLENISHMENT_SOURCE_HEADER_SX, REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX, REPLENISHMENT_TEXTFIELD_SX, ReplenishmentFieldLabel, + ReplenishmentFilterField, ReplenishmentItemEntryPlainText, ReplenishmentTextField, replenishmentSearchGridInputSx, @@ -230,6 +232,8 @@ const DoReplenishmentTab: React.FC = () => { const [isLoadingTracking, setIsLoadingTracking] = useState(false); const [trackStatusFilter, setTrackStatusFilter] = useState("all"); const [trackDateFilter, setTrackDateFilter] = useState(null); + const [trackPage, setTrackPage] = useState(0); + const [trackRowsPerPage, setTrackRowsPerPage] = useState(10); const [trackingDialogOpen, setTrackingDialogOpen] = useState(false); const deliveryDateStr = deliveryDate?.format("YYYY-MM-DD") ?? ""; @@ -439,6 +443,7 @@ const DoReplenishmentTab: React.FC = () => { status: trackStatusFilter, }); setRecords(data.map(mapApiRecord)); + setTrackPage(0); } catch { await Swal.fire({ icon: "error", title: t("Failed to load replenishment records") }); } finally { @@ -452,51 +457,6 @@ const DoReplenishmentTab: React.FC = () => { } }, [trackingDialogOpen, loadTrackingRecords]); - const trackColumns: GridColDef[] = 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() @@ -504,6 +464,11 @@ const DoReplenishmentTab: React.FC = () => { : t("Truck X") : ""; + const paginatedTrackRecords = useMemo(() => { + const start = trackPage * trackRowsPerPage; + return records.slice(start, start + trackRowsPerPage); + }, [records, trackPage, trackRowsPerPage]); + const datePickerSlotProps = useMemo( () => ({ textField: { @@ -746,27 +711,10 @@ const DoReplenishmentTab: React.FC = () => { (theme.palette.mode === "dark" ? "grey.900" : "grey.50"), }} > - - setTrackingDialogOpen(true)} - aria-label={t("Replenishment Tracking")} - sx={{ - position: "absolute", - top: 8, - right: 8, - zIndex: 1, - color: "text.secondary", - }} - > - - - { columnGap: 2, rowGap: 1, alignItems: "stretch", - pr: { xs: 4, lg: 4 }, }} > { } }} /> - + + + + @@ -1145,45 +1103,139 @@ const DoReplenishmentTab: React.FC = () => { - - - - + (theme.palette.mode === "dark" ? "grey.900" : "grey.50"), + }} + > + + + } + title={t("Estimated Arrival Date")} + > setTrackDateFilter(v)} + onChange={(v) => { + setTrackDateFilter(v); + setTrackPage(0); + }} slotProps={datePickerSlotProps} + sx={{ width: "100%" }} /> - - - {t("Status")} - - - - - + + + + + + ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: 2, + bgcolor: theme.palette.mode === "dark" ? "grey.900" : "common.white", + opacity: isLoadingTracking ? 0.6 : 1, + pointerEvents: isLoadingTracking ? "none" : "auto", + })} + > + + + + {t("Replenishment Code")} + {t("Source DO")} + {t("Shop Name")} + {t("Truck Lance Code")} + {t("Item No.")} + {t("Item Name")} + + {t("Replenish Qty")} + + {t("uom")} + {t("Target DO")} + {t("Status")} + {t("Created")} + + + + {paginatedTrackRecords.length === 0 ? ( + + + {isLoadingTracking ? t("Loading") : t("No data")} + + + ) : ( + paginatedTrackRecords.map((row) => ( + + {row.code} + {row.sourceDoCode} + {row.shopName ?? "—"} + + {row.truckLaneCode ?? "—"} + + {row.itemNo} + {row.itemName} + {row.replenishQty} + {row.shortUom ?? "—"} + {row.targetDoCode ?? "—"} + {t(row.status)} + + {row.created + ? dayjs(row.created).format("YYYY-MM-DD HH:mm") + : "—"} + + + )) + )} + +
+ setTrackPage(page)} + rowsPerPage={trackRowsPerPage} + onRowsPerPageChange={(e) => { + setTrackRowsPerPage(Number(e.target.value)); + setTrackPage(0); + }} + rowsPerPageOptions={[10, 25, 50]} + labelRowsPerPage={t("Rows per page")} + /> +
+
diff --git a/src/components/DoSearch/ReplenishmentFilterField.tsx b/src/components/DoSearch/ReplenishmentFilterField.tsx index d8bc265..369b3cd 100644 --- a/src/components/DoSearch/ReplenishmentFilterField.tsx +++ b/src/components/DoSearch/ReplenishmentFilterField.tsx @@ -153,6 +153,23 @@ export function ReplenishmentTextField(props: ReplenishmentTextFieldProps) { ); } +/** Filled select matching {@link ReplenishmentTextField} border and padding. */ +export const REPLENISHMENT_FILLED_SELECT_SX = (theme: Theme) => + ({ + ...REPLENISHMENT_TEXTFIELD_SX(theme), + "& .MuiFilledInput-root": { + alignItems: "center", + borderRadius: 2, + bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", + border: `1px solid ${theme.palette.divider}`, + "&::before, &::after": { display: "none" }, + }, + "& .MuiSelect-select": { + paddingTop: REPLENISHMENT_FIELD_BODY_PY, + paddingBottom: REPLENISHMENT_FIELD_BODY_PY, + }, + }) as const; + /** Read-only item row value — blank until a line is selected. */ export function ReplenishmentItemEntryPlainText({ value, @@ -418,7 +435,7 @@ export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) => ({ textTransform: "none", whiteSpace: "nowrap", flexShrink: 0, - minWidth: { xs: "100%", lg: 108 }, + minWidth: { xs: "auto", lg: 108 }, "& .MuiButton-startIcon": { margin: 0, marginRight: theme.spacing(0.75), @@ -428,3 +445,16 @@ export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) => ({ }, }); +/** 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", + }, +}); + diff --git a/src/i18n/en/do.json b/src/i18n/en/do.json index 56f9ce4..9d14976 100644 --- a/src/i18n/en/do.json +++ b/src/i18n/en/do.json @@ -48,6 +48,8 @@ "Location": "Location", "Lot No.": "Lot No.", "No trucks available": "No trucks available", + "No data": "No data", + "Rows per page": "Rows per page", "Order Date": "Order Date", "Order Date From": "Order Date From", "Order Date To": "Order Date To", diff --git a/src/i18n/zh/do.json b/src/i18n/zh/do.json index a3dedc5..4dce65b 100644 --- a/src/i18n/zh/do.json +++ b/src/i18n/zh/do.json @@ -72,6 +72,8 @@ "Loading": "正在加載...", "No delivery orders selected for batch release. Uncheck orders you want to exclude, or search again to reset selection.": "沒有選擇送貨訂單進行批量放單。取消勾選您想排除的訂單,或重新搜索以重置選擇。", "No Records": "沒有找到記錄", + "No data": "沒有資料", + "Rows per page": "每頁數量", "OK": "確認", "Truck X": "車線-X", "Order Date From": "訂單日期",