@@ -10,7 +10,6 @@ import {
DialogTitle,
DialogTitle,
FormControl,
FormControl,
IconButton,
IconButton,
InputLabel,
MenuItem,
MenuItem,
Paper,
Paper,
Select,
Select,
@@ -20,11 +19,13 @@ import {
TableCell,
TableCell,
TableContainer,
TableContainer,
TableHead,
TableHead,
TablePagination,
TableRow,
TableRow,
Tooltip,
Tooltip,
Typography,
Typography,
} from "@mui/material";
} from "@mui/material";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import FilterListIcon from "@mui/icons-material/FilterList";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import StorefrontIcon from "@mui/icons-material/Storefront";
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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import dayjs, { Dayjs } from "dayjs";
import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import { GridColDef } from "@mui/x-data-grid";
import Swal from "sweetalert2";
import Swal from "sweetalert2";
import StyledDataGrid from "../StyledDataGrid";
import {
import {
DoDetail,
DoDetail,
DoDetailLine,
DoDetailLine,
@@ -48,15 +47,18 @@ import {
import { arrayToDateString } from "@/app/utils/formatUtil";
import { arrayToDateString } from "@/app/utils/formatUtil";
import {
import {
REPLENISHMENT_FIELD_ICON_SX,
REPLENISHMENT_FIELD_ICON_SX,
REPLENISHMENT_FILLED_SELECT_SX,
REPLENISHMENT_TABLE_AUTOCOMPLETE_SX,
REPLENISHMENT_TABLE_AUTOCOMPLETE_SX,
REPLENISHMENT_TABLE_ENTRY_ROW_SX,
REPLENISHMENT_TABLE_ENTRY_ROW_SX,
REPLENISHMENT_TABLE_INLINE_TEXTFIELD_SX,
REPLENISHMENT_TABLE_INLINE_TEXTFIELD_SX,
REPLENISHMENT_TABLE_SX,
REPLENISHMENT_TABLE_SX,
REPLENISHMENT_LOOKUP_BUTTON_SX,
REPLENISHMENT_LOOKUP_BUTTON_SX,
REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX,
REPLENISHMENT_SOURCE_HEADER_SX,
REPLENISHMENT_SOURCE_HEADER_SX,
REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX,
REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX,
REPLENISHMENT_TEXTFIELD_SX,
REPLENISHMENT_TEXTFIELD_SX,
ReplenishmentFieldLabel,
ReplenishmentFieldLabel,
ReplenishmentFilterField,
ReplenishmentItemEntryPlainText,
ReplenishmentItemEntryPlainText,
ReplenishmentTextField,
ReplenishmentTextField,
replenishmentSearchGridInputSx,
replenishmentSearchGridInputSx,
@@ -230,6 +232,8 @@ const DoReplenishmentTab: React.FC = () => {
const [isLoadingTracking, setIsLoadingTracking] = useState(false);
const [isLoadingTracking, setIsLoadingTracking] = useState(false);
const [trackStatusFilter, setTrackStatusFilter] = useState<ReplenishmentStatus | "all">("all");
const [trackStatusFilter, setTrackStatusFilter] = useState<ReplenishmentStatus | "all">("all");
const [trackDateFilter, setTrackDateFilter] = useState<Dayjs | null>(null);
const [trackDateFilter, setTrackDateFilter] = useState<Dayjs | null>(null);
const [trackPage, setTrackPage] = useState(0);
const [trackRowsPerPage, setTrackRowsPerPage] = useState(10);
const [trackingDialogOpen, setTrackingDialogOpen] = useState(false);
const [trackingDialogOpen, setTrackingDialogOpen] = useState(false);
const deliveryDateStr = deliveryDate?.format("YYYY-MM-DD") ?? "";
const deliveryDateStr = deliveryDate?.format("YYYY-MM-DD") ?? "";
@@ -439,6 +443,7 @@ const DoReplenishmentTab: React.FC = () => {
status: trackStatusFilter,
status: trackStatusFilter,
});
});
setRecords(data.map(mapApiRecord));
setRecords(data.map(mapApiRecord));
setTrackPage(0);
} catch {
} catch {
await Swal.fire({ icon: "error", title: t("Failed to load replenishment records") });
await Swal.fire({ icon: "error", title: t("Failed to load replenishment records") });
} finally {
} finally {
@@ -452,51 +457,6 @@ const DoReplenishmentTab: React.FC = () => {
}
}
}, [trackingDialogOpen, 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 selectedLineUom = lineUomDisplay(selectedLine);
const sourceTruckLaneDisplay = sourceDo
const sourceTruckLaneDisplay = sourceDo
? sourceDo.truckLaneCode?.trim()
? sourceDo.truckLaneCode?.trim()
@@ -504,6 +464,11 @@ const DoReplenishmentTab: React.FC = () => {
: t("Truck X")
: t("Truck X")
: "";
: "";
const paginatedTrackRecords = useMemo(() => {
const start = trackPage * trackRowsPerPage;
return records.slice(start, start + trackRowsPerPage);
}, [records, trackPage, trackRowsPerPage]);
const datePickerSlotProps = useMemo(
const datePickerSlotProps = useMemo(
() => ({
() => ({
textField: {
textField: {
@@ -746,27 +711,10 @@ const DoReplenishmentTab: React.FC = () => {
<Paper
<Paper
variant="outlined"
variant="outlined"
sx={{
sx={{
position: "relative",
p: 2,
p: 2,
bgcolor: (theme) => (theme.palette.mode === "dark" ? "grey.900" : "grey.50"),
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}>
<Stack spacing={2}>
<Box
<Box
sx={{
sx={{
@@ -775,7 +723,6 @@ const DoReplenishmentTab: React.FC = () => {
columnGap: 2,
columnGap: 2,
rowGap: 1,
rowGap: 1,
alignItems: "stretch",
alignItems: "stretch",
pr: { xs: 4, lg: 4 },
}}
}}
>
>
<ReplenishmentFieldLabel
<ReplenishmentFieldLabel
@@ -844,16 +791,27 @@ const DoReplenishmentTab: React.FC = () => {
}
}
}}
}}
/>
/>
<Button
variant="contained"
disableElevation
startIcon={<Search fontSize="small" />}
onClick={() => void handleLookupSourceDo()}
disabled={isLookingUp}
sx={REPLENISHMENT_LOOKUP_BUTTON_SX}
>
{t("Lookup")}
</Button>
<Stack direction="row" spacing={1} sx={{ flexShrink: 0, alignSelf: "stretch" }}>
<Button
variant="contained"
disableElevation
startIcon={<Search fontSize="small" />}
onClick={() => void handleLookupSourceDo()}
disabled={isLookingUp}
sx={REPLENISHMENT_LOOKUP_BUTTON_SX}
>
{t("Lookup")}
</Button>
<Button
variant="outlined"
disableElevation
startIcon={<InfoOutlinedIcon fontSize="small" />}
onClick={() => setTrackingDialogOpen(true)}
sx={REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX}
>
{t("Replenishment Tracking")}
</Button>
</Stack>
</Box>
</Box>
</Box>
</Box>
@@ -1145,45 +1103,139 @@ const DoReplenishmentTab: React.FC = () => {
<Close fontSize="small" />
<Close fontSize="small" />
</IconButton>
</IconButton>
</DialogTitle>
</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 }}>
<DialogContent
sx={{
p: 2,
bgcolor: (theme) => (theme.palette.mode === "dark" ? "grey.900" : "grey.50"),
}}
>
<Stack spacing={2}>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<ReplenishmentFilterField
icon={<CalendarTodayIcon fontSize="small" sx={REPLENISHMENT_FIELD_ICON_SX} />}
title={t("Estimated Arrival Date")}
>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
<DatePicker
format="YYYY-MM-DD"
format="YYYY-MM-DD"
value={trackDateFilter}
value={trackDateFilter}
onChange={(v) => setTrackDateFilter(v)}
onChange={(v) => {
setTrackDateFilter(v);
setTrackPage(0);
}}
slotProps={datePickerSlotProps}
slotProps={datePickerSlotProps}
sx={{ width: "100%" }}
/>
/>
</LocalizationProvider>
</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")
}
</ReplenishmentFilterField>
<ReplenishmentFilterField
icon={<FilterListIcon fontSize="small" sx={REPLENISHMENT_FIELD_ICON_SX} />}
title={t("Status")}
>
<FormControl
fullWidth
size="small"
variant="filled"
hiddenLabel
sx={REPLENISHMENT_FILLED_SELECT_SX}
>
>
<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 } } }}
/>
<Select
value={trackStatusFilter}
onChange={(e) => {
setTrackStatusFilter(e.target.value as ReplenishmentStatus | "all");
setTrackPage(0);
}}
disableUnderline
>
<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>
</ReplenishmentFilterField>
</Box>
<TableContainer
sx={(theme) => ({
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",
})}
>
<Table size="small" sx={REPLENISHMENT_TABLE_SX}>
<TableHead>
<TableRow>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Replenishment Code")}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Source DO")}</TableCell>
<TableCell>{t("Shop Name")}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Truck Lance Code")}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Item No.")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
{t("Replenish Qty")}
</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("uom")}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Target DO")}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Status")}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t("Created")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedTrackRecords.length === 0 ? (
<TableRow>
<TableCell colSpan={11} align="center" sx={{ py: 4, color: "text.secondary" }}>
{isLoadingTracking ? t("Loading") : t("No data")}
</TableCell>
</TableRow>
) : (
paginatedTrackRecords.map((row) => (
<TableRow key={row.id} hover>
<TableCell sx={{ whiteSpace: "nowrap" }}>{row.code}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{row.sourceDoCode}</TableCell>
<TableCell sx={{ wordBreak: "break-word" }}>{row.shopName ?? "—"}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>
{row.truckLaneCode ?? "—"}
</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{row.itemNo}</TableCell>
<TableCell sx={{ wordBreak: "break-word" }}>{row.itemName}</TableCell>
<TableCell align="right">{row.replenishQty}</TableCell>
<TableCell>{row.shortUom ?? "—"}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{row.targetDoCode ?? "—"}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>{t(row.status)}</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>
{row.created
? dayjs(row.created).format("YYYY-MM-DD HH:mm")
: "—"}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
component="div"
count={records.length}
page={trackPage}
onPageChange={(_, page) => setTrackPage(page)}
rowsPerPage={trackRowsPerPage}
onRowsPerPageChange={(e) => {
setTrackRowsPerPage(Number(e.target.value));
setTrackPage(0);
}}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
/>
</TableContainer>
</Stack>
</DialogContent>
</DialogContent>
</Dialog>
</Dialog>
</Stack>
</Stack>