Преглед изворни кода

improvement replenishment

production
kelvin.yau пре 1 недеља
родитељ
комит
a81fff570b
4 измењених фајлова са 194 додато и 108 уклоњено
  1. +159
    -107
      src/components/DoSearch/DoReplenishmentTab.tsx
  2. +31
    -1
      src/components/DoSearch/ReplenishmentFilterField.tsx
  3. +2
    -0
      src/i18n/en/do.json
  4. +2
    -0
      src/i18n/zh/do.json

+ 159
- 107
src/components/DoSearch/DoReplenishmentTab.tsx Прегледај датотеку

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


+ 31
- 1
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. */ /** Read-only item row value — blank until a line is selected. */
export function ReplenishmentItemEntryPlainText({ export function ReplenishmentItemEntryPlainText({
value, value,
@@ -418,7 +435,7 @@ export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) => ({
textTransform: "none", textTransform: "none",
whiteSpace: "nowrap", whiteSpace: "nowrap",
flexShrink: 0, flexShrink: 0,
minWidth: { xs: "100%", lg: 108 },
minWidth: { xs: "auto", lg: 108 },
"& .MuiButton-startIcon": { "& .MuiButton-startIcon": {
margin: 0, margin: 0,
marginRight: theme.spacing(0.75), 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",
},
});


+ 2
- 0
src/i18n/en/do.json Прегледај датотеку

@@ -48,6 +48,8 @@
"Location": "Location", "Location": "Location",
"Lot No.": "Lot No.", "Lot No.": "Lot No.",
"No trucks available": "No trucks available", "No trucks available": "No trucks available",
"No data": "No data",
"Rows per page": "Rows per page",
"Order Date": "Order Date", "Order Date": "Order Date",
"Order Date From": "Order Date From", "Order Date From": "Order Date From",
"Order Date To": "Order Date To", "Order Date To": "Order Date To",


+ 2
- 0
src/i18n/zh/do.json Прегледај датотеку

@@ -72,6 +72,8 @@
"Loading": "正在加載...", "Loading": "正在加載...",
"No delivery orders selected for batch release. Uncheck orders you want to exclude, or search again to reset selection.": "沒有選擇送貨訂單進行批量放單。取消勾選您想排除的訂單,或重新搜索以重置選擇。", "No delivery orders selected for batch release. Uncheck orders you want to exclude, or search again to reset selection.": "沒有選擇送貨訂單進行批量放單。取消勾選您想排除的訂單,或重新搜索以重置選擇。",
"No Records": "沒有找到記錄", "No Records": "沒有找到記錄",
"No data": "沒有資料",
"Rows per page": "每頁數量",
"OK": "確認", "OK": "確認",
"Truck X": "車線-X", "Truck X": "車線-X",
"Order Date From": "訂單日期", "Order Date From": "訂單日期",


Loading…
Откажи
Сачувај