Explorar el Código

replenishment update

production
kelvin.yau hace 1 semana
padre
commit
96c8ac643b
Se han modificado 5 ficheros con 1127 adiciones y 623 borrados
  1. +6
    -0
      src/app/api/do/actions.tsx
  2. +859
    -540
      src/components/DoSearch/DoReplenishmentTab.tsx
  3. +206
    -59
      src/components/DoSearch/ReplenishmentFilterField.tsx
  4. +28
    -12
      src/i18n/en/do.json
  5. +28
    -12
      src/i18n/zh/do.json

+ 6
- 0
src/app/api/do/actions.tsx Ver fichero

@@ -38,6 +38,8 @@ export interface DoDetailLine {
id: number;
itemNo: string;
qty: number;
/** Sum of stock_out_line qty for linked pick order line; falls back to qty. */
actualShippedQty?: number;
price: number;
status: string;
itemName?: string;
@@ -680,6 +682,7 @@ export interface SubmitDoReplenishmentLineRequest {
sourceDoLineId: number;
replenishQty: number;
truckLaneCode?: string;
reason?: string;
}

export interface DoReplenishmentRecord {
@@ -692,6 +695,7 @@ export interface DoReplenishmentRecord {
itemId: number;
itemNo?: string;
itemName?: string;
originalQty?: number;
replenishQty: number;
shortUom?: string;
shopCode?: string;
@@ -699,8 +703,10 @@ export interface DoReplenishmentRecord {
truckLaneCode?: string;
targetDoId?: number;
targetDoCode?: string;
targetDoEstimatedArrivalDate?: string;
pickOrderLineId?: number;
status: string;
reason?: string;
created?: string;
}



+ 859
- 540
src/components/DoSearch/DoReplenishmentTab.tsx
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 206
- 59
src/components/DoSearch/ReplenishmentFilterField.tsx Ver fichero

@@ -11,7 +11,7 @@ export const REPLENISHMENT_FIELD_LABEL_SX = (theme: Theme) => ({
theme.palette.mode === "dark"
? theme.palette.grey[100]
: theme.palette.common.black,
fontWeight: 600,
fontWeight: 700,
});

export const REPLENISHMENT_FIELD_ICON_SX = (theme: Theme) => ({
@@ -28,6 +28,7 @@ export const REPLENISHMENT_TEXTFIELD_SX = (theme: Theme) =>
borderRadius: 2,
bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white",
border: `1px solid ${theme.palette.divider}`,
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
},
"& .MuiFilledInput-root.Mui-focused": {
bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white",
@@ -56,6 +57,7 @@ export const REPLENISHMENT_AUTOCOMPLETE_SX = (theme: Theme) =>
width: "100%",
},
"& .MuiAutocomplete-inputRoot": {
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
paddingTop: `${REPLENISHMENT_FIELD_BODY_PY} !important`,
paddingBottom: `${REPLENISHMENT_FIELD_BODY_PY} !important`,
paddingLeft: `${theme.spacing(REPLENISHMENT_FIELD_BODY_PX)} !important`,
@@ -75,6 +77,75 @@ export const REPLENISHMENT_FIELD_BODY_PY = "12px";
/** Horizontal padding aligned with MUI filled input (spacing 1.5 = 12px). */
export const REPLENISHMENT_FIELD_BODY_PX = 1.5;

/** Fixed height for replenishment inputs, selects, and read-only value boxes. */
export const REPLENISHMENT_FIELD_CONTROL_HEIGHT = 44;

export const REPLENISHMENT_FIELD_CONTROL_ROOT_SX = {
height: REPLENISHMENT_FIELD_CONTROL_HEIGHT,
minHeight: REPLENISHMENT_FIELD_CONTROL_HEIGHT,
maxHeight: REPLENISHMENT_FIELD_CONTROL_HEIGHT,
boxSizing: "border-box" as const,
};

/** Read-only value box — same outer height as {@link ReplenishmentTextField}. */
export const REPLENISHMENT_READONLY_VALUE_SX = (theme: Theme) =>
({
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white",
px: REPLENISHMENT_FIELD_BODY_PX,
display: "flex",
alignItems: "center",
minWidth: 0,
overflow: "hidden",
}) as const;

export function ReplenishmentReadonlyValue({
children,
fontWeight,
}: {
children: React.ReactNode;
fontWeight?: number;
}) {
return (
<Box sx={REPLENISHMENT_READONLY_VALUE_SX}>
<Typography
variant="body2"
component="div"
fontWeight={fontWeight}
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
width: "100%",
minWidth: 0,
lineHeight: 1.43,
}}
>
{children ?? "\u00A0"}
</Typography>
</Box>
);
}

/** Invisible label spacer so action buttons align with labelled fields. */
export function ReplenishmentFieldLabelSpacer() {
return (
<Typography
variant="body2"
aria-hidden
sx={{
visibility: "hidden",
lineHeight: 1.35,
userSelect: "none",
}}
>
{"\u00A0"}
</Typography>
);
}

/** Source DO summary header: same inset as textbox content area. */
export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) =>
({
@@ -89,7 +160,7 @@ export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) =>
}) as const;

type ReplenishmentFieldLabelProps = {
icon: ReactNode;
icon?: ReactNode;
title: string;
required?: boolean;
sx?: SxProps<Theme>;
@@ -102,14 +173,22 @@ export function ReplenishmentFieldLabel({
sx,
}: ReplenishmentFieldLabelProps) {
return (
<Stack direction="row" spacing={1} alignItems="center" sx={sx}>
{icon}
<Typography variant="body2" sx={REPLENISHMENT_FIELD_LABEL_SX} component="span">
<Stack direction="row" spacing={icon ? 1 : 0} alignItems="center" sx={sx}>
{icon ?? null}
<Typography
variant="body2"
sx={(theme) => ({
...REPLENISHMENT_FIELD_LABEL_SX(theme),
whiteSpace: "normal",
lineHeight: 1.35,
})}
component="span"
>
{title}
{required ? (
<Typography component="span" color="error.main" aria-hidden="true">
<Box component="span" color="error.main" aria-hidden="true">
{" *"}
</Typography>
</Box>
) : null}
</Typography>
</Stack>
@@ -163,10 +242,13 @@ export const REPLENISHMENT_FILLED_SELECT_SX = (theme: Theme) =>
bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white",
border: `1px solid ${theme.palette.divider}`,
"&::before, &::after": { display: "none" },
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
},
"& .MuiSelect-select": {
paddingTop: REPLENISHMENT_FIELD_BODY_PY,
paddingBottom: REPLENISHMENT_FIELD_BODY_PY,
display: "flex",
alignItems: "center",
},
}) as const;

@@ -242,17 +324,57 @@ export function ReplenishmentQtyWithUomField({
);
}

/** Tracking dialog table — horizontal scroll, no fixed layout (avoids column text stacking). */
export const REPLENISHMENT_TRACKING_TABLE_SX = {
width: "max-content",
minWidth: "100%",
"& .MuiTableCell-root": {
typography: "body2",
borderColor: "divider",
py: 1,
px: 1.25,
whiteSpace: "nowrap",
},
"& .MuiTableCell-root:first-of-type": {
pl: 1.5,
},
"& .MuiTableHead-root .MuiTableCell-root": {
fontWeight: 600,
color: "text.secondary",
bgcolor: "action.hover",
borderBottom: "1px solid",
borderColor: "divider",
},
"& .MuiTableBody-root .MuiTableRow-root:not(:last-of-type) .MuiTableCell-root": {
borderBottom: "1px solid",
borderColor: "divider",
},
} as const;

export const REPLENISHMENT_TRACKING_CELL_ELLIPSIS_SX = {
maxWidth: 160,
overflow: "hidden",
textOverflow: "ellipsis",
} as const;

export const REPLENISHMENT_TRACKING_CELL_WRAP_SX = {
minWidth: 120,
maxWidth: 200,
whiteSpace: "normal",
wordBreak: "break-word",
} as const;

export const REPLENISHMENT_TABLE_SX = {
tableLayout: { md: "fixed" },
width: "100%",
tableLayout: "fixed",
"& .MuiTableCell-root": {
typography: "body2",
borderColor: "divider",
py: 1.25,
px: 2,
py: 1,
px: 1.25,
},
"& .MuiTableCell-root:first-of-type": {
pl: 3.5,
pl: 1.5,
},
"& .MuiTableHead-root .MuiTableCell-root": {
fontWeight: 600,
@@ -378,10 +500,27 @@ export const REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX = {
width: "100%",
} as const;

/** In-table select — compact padding; truncate long selected labels. */
export const REPLENISHMENT_TABLE_INLINE_SELECT_SX = (theme: Theme) =>
({
...REPLENISHMENT_FILLED_SELECT_SX(theme),
"& .MuiSelect-select": {
paddingTop: "6px",
paddingBottom: "6px",
paddingLeft: theme.spacing(1),
paddingRight: `${theme.spacing(3)} !important`,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
}) as const;

export const replenishmentSearchGridLabelSx = (col: number) => ({
gridColumn: { xs: 1, lg: col },
gridRow: { xs: "auto", lg: 1 },
minWidth: 0,
minWidth: "min-content",
overflow: "hidden",
textOverflow: "ellipsis",
});

export const replenishmentSearchGridInputSx = (col: number) => ({
@@ -395,66 +534,74 @@ export const replenishmentSearchGridInputSx = (col: number) => ({
},
});

/** Shop input + lookup button share one row; button height follows the textbox. */
export const replenishmentSearchGridShopRowSx = {
gridColumn: { xs: 1, lg: 3 },
/** Lookup / tracking buttons beside the three filter inputs (4th grid column on lg). */
export const replenishmentSearchGridActionsSx = {
gridColumn: { xs: 1, lg: 4 },
gridRow: { xs: "auto", lg: 2 },
minWidth: 0,
display: "flex",
justifyContent: { xs: "stretch", lg: "flex-start" },
alignItems: "stretch",
gap: 1,
"& .MuiTextField-root": {
flex: 1,
minWidth: 0,
},
"& .MuiFormControl-root": {
height: "100%",
},
"& .MuiFilledInput-root": {
height: "100%",
boxSizing: "border-box",
flexWrap: { xs: "wrap", lg: "nowrap" },
gap: 1.5,
minWidth: 0,
"& .MuiButton-root": {
flex: { xs: 1, lg: "0 0 auto" },
alignSelf: "stretch",
},
};

/** Match {@link ReplenishmentFieldLabel} typography on contained buttons. */
/** Match {@link ReplenishmentFieldLabel} typography on field-height buttons. */
export const REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX = (theme: Theme) => ({
fontSize: theme.typography.body2.fontSize,
fontWeight: 600,
lineHeight: 1,
});

export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) => ({
...REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX(theme),
alignSelf: "stretch",
minHeight: "unset",
height: "auto",
py: 0,
px: 1.5,
borderRadius: 2,
boxShadow: "none",
textTransform: "none",
whiteSpace: "nowrap",
flexShrink: 0,
minWidth: { xs: "auto", lg: 108 },
"& .MuiButton-startIcon": {
margin: 0,
marginRight: theme.spacing(0.75),
"& > *:nth-of-type(1)": {
fontSize: 20,
/** Base button style — same 44px height as {@link ReplenishmentTextField}. */
export const REPLENISHMENT_FIELD_BUTTON_SX = (theme: Theme) =>
({
...REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX(theme),
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
paddingTop: 0,
paddingBottom: 0,
px: REPLENISHMENT_FIELD_BODY_PX,
borderRadius: 2,
boxShadow: "none",
textTransform: "none",
whiteSpace: "nowrap",
flexShrink: 0,
"&.MuiButton-root": {
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
},
},
});
"& .MuiButton-startIcon": {
margin: 0,
marginRight: theme.spacing(0.75),
"& > *:nth-of-type(1)": {
fontSize: 18,
},
},
}) as const;

export const REPLENISHMENT_LOOKUP_BUTTON_SX = (theme: Theme) =>
({
...REPLENISHMENT_FIELD_BUTTON_SX(theme),
alignSelf: "stretch",
px: REPLENISHMENT_FIELD_BODY_PX,
minWidth: { xs: "auto", lg: 108 },
}) as const;

/** 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",
},
});
export const REPLENISHMENT_OUTLINED_ACTION_BUTTON_SX = (theme: Theme) =>
({
...REPLENISHMENT_FIELD_BUTTON_SX(theme),
minWidth: "auto",
px: REPLENISHMENT_FIELD_BODY_PX,
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",
},
}) as const;


+ 28
- 12
src/i18n/en/do.json Ver fichero

@@ -73,27 +73,35 @@
"Delivery Date": "Delivery Date",
"Enter last 4 characters of DO code": "Enter last 4 characters of DO code",
"Shop code, or first characters of shop name": "Shop code (partial match), or first characters of shop name",
"Multiple source DOs matched": "Multiple source DOs matched",
"Multiple source DOs matched": "Multiple original DOs matched",
"Please verify DO code suffix, delivery date and shop.": "Please verify DO code suffix, delivery date and shop.",
"Shop code or name is required": "Shop code or name is required",
"Draft List": "Draft List",
"Replenishment preview hint": "Add items from different source DOs, then batch submit from here.",
"Replenishment preview empty": "Added items appear here. Look up another source DO to keep adding.",
"Replenishment preview hint": "Add items from different original DOs, then batch submit from here.",
"Replenishment preview empty": "Added items appear here. Match another original DO to keep adding.",
"replenishmentCurrentDoDraftHint": "Added to draft list ({{count}} for this DO)",
"replenishmentTargetDoEstimatedArrivalDate": "Target DO Estimated Arrival Date",
"replenishmentOriginalSourceDoCode": "Original DO Code",
"replenishmentTargetDoCode": "Target DO Code",
"replenishmentStatusLabel": "Replenishment Status",
"replenishmentItemInfo": "Item Information",
"Clear": "Clear",
"Enter item code to search": "Enter item code to search",
"Failed to lookup source DO": "Failed to lookup source DO",
"Failed to lookup source DO": "Failed to match original DO",
"Item": "Item",
"Lookup": "Lookup",
"Lookup": "Match DO",
"No draft rows to submit": "No draft rows to submit",
"Only completed delivery orders can be used as replenishment source.": "Only completed delivery orders can be used as replenishment source.",
"Only completed delivery orders can be used as replenishment source.": "Only completed delivery orders can be used as the original DO.",
"Original Shipment Qty": "Original Shipment Qty",
"Please lookup source DO first": "Please lookup source DO first",
"Original Shipment Qty short": "Orig. Qty",
"Please lookup source DO first": "Please match original DO first",
"Picker Name": "Picker Name",
"Please select an item": "Please select an item",
"Records saved locally for preview. Backend integration pending.": "Records saved locally for preview. Backend integration pending.",
"Replenishment Code": "Replenishment No.",
"Ref Code": "Ref Code",
"Replenish Qty": "Replenish Qty",
"Replenish Qty short": "Replenish",
"Replenish qty must be greater than zero": "Replenish qty must be greater than zero",
"Replenishment": "Replenishment",
"Delivery date is required": "Delivery date is required",
@@ -111,11 +119,18 @@
"replenishmentDatePlaceholder": "YYYY-MM-DD",
"replenishmentDoSuffixPlaceholder": "DO No. (last 4)",
"replenishmentShopPlaceholder": "Shop Code",
"Source DO": "Source DO",
"Source DO Code": "Source DO Code",
"Source DO code is required": "Source DO code is required",
"Source DO must be completed": "Source DO must be completed",
"Source DO not found": "Source DO not found",
"replenishmentRemarkPlaceholder": "Optional",
"replenishmentRemarkShort": "Optional",
"replenishmentReason": {
"quality_issue": "Quality issue",
"out_of_stock": "Out of stock",
"other": "Other"
},
"Source DO": "Original DO",
"Source DO Code": "Original DO Code",
"Source DO code is required": "Original DO code is required",
"Source DO must be completed": "Original DO must be completed",
"Source DO not found": "Original DO not found",
"Submit": "Submit",
"Target DO": "Target DO",
"This item is already in the draft list": "This item is already in the draft list",
@@ -144,6 +159,7 @@
"Supplier Name": "Supplier Name",
"Truck Availability Warning": "Truck Availability Warning",
"Truck Lance Code": "Truck Lance Code",
"Truck Lane": "Truck Lane",
"Truck X": "Truck X",
"Truck lane search requires date message": "Truck lane search requires date message",
"Truck lane search requires date title": "Truck lane search requires date title",


+ 28
- 12
src/i18n/zh/do.json Ver fichero

@@ -19,26 +19,34 @@
"Enter last 4 characters of DO code": "請輸入送貨單號末四位",
"Enter item code to search": "輸入貨品編號搜尋",
"Shop code, or first characters of shop name": "店鋪代碼(部分符合),或店鋪名稱開頭字元",
"Multiple source DOs matched": "找到多張符合的來源送貨單",
"Multiple source DOs matched": "找到多張符合的送貨單",
"Please verify DO code suffix, delivery date and shop.": "請核對送貨單號末四位、送貨日及店鋪資料。",
"Shop code or name is required": "請輸入店鋪代碼或名稱",
"Draft List": "待提交列表",
"Replenishment preview hint": "可從不同來源送貨單加入品項,在此批次提交。",
"Replenishment preview empty": "加入的品項會顯示於此;可再查詢其他來源送貨單繼續加入。",
"Replenishment preview hint": "可從不同原送貨單加入品項,在此批次提交。",
"Replenishment preview empty": "加入的品項會顯示於此;可再對單其他原送貨單繼續加入。",
"replenishmentCurrentDoDraftHint": "已加入待提交列表(此送貨單 {{count}} 項)",
"replenishmentTargetDoEstimatedArrivalDate": "目標送貨單預計送貨日期",
"replenishmentOriginalSourceDoCode": "原送貨單編號",
"replenishmentTargetDoCode": "目標送貨單編號",
"replenishmentStatusLabel": "補貨狀態",
"replenishmentItemInfo": "貨品資訊",
"Clear": "清空",
"Failed to lookup source DO": "查詢來源送貨單失敗",
"Failed to lookup source DO": "原送貨單對單失敗",
"Item": "物品",
"Lookup": "查詢",
"Lookup": "對單",
"No draft rows to submit": "沒有待提交的行",
"Only completed delivery orders can be used as replenishment source.": "只有已送貨(completed)的送貨單可作為補貨來源。",
"Only completed delivery orders can be used as replenishment source.": "只有已送貨(completed)的送貨單可作為原送貨單。",
"Original Shipment Qty": "原出貨數",
"Please lookup source DO first": "請先查詢來源送貨單",
"Original Shipment Qty short": "原出貨",
"Please lookup source DO first": "請先對單(原送貨單)",
"Picker Name": "揀貨員名稱",
"Please select an item": "請選擇物品",
"Records saved locally for preview. Backend integration pending.": "記錄已暫存於本地預覽,後端 API 尚未就緒。",
"Replenishment Code": "補貨編號",
"Ref Code": "參考編號",
"Replenish Qty": "補貨數量",
"Replenish Qty short": "補貨",
"Replenish qty must be greater than zero": "補貨數量必須大於零",
"Replenishment": "補貨",
"Delivery date is required": "請選擇送貨日期",
@@ -56,11 +64,18 @@
"replenishmentDatePlaceholder": "YYYY-MM-DD",
"replenishmentDoSuffixPlaceholder": "送貨單號末四位",
"replenishmentShopPlaceholder": "店鋪編號",
"Source DO": "來源送貨單",
"Source DO Code": "來源送貨單編號",
"Source DO code is required": "請輸入來源送貨單編號",
"Source DO must be completed": "來源送貨單須為已送貨狀態",
"Source DO not found": "找不到來源送貨單",
"replenishmentRemarkPlaceholder": "請選擇(選填)",
"replenishmentRemarkShort": "選填",
"replenishmentReason": {
"quality_issue": "質素問題",
"out_of_stock": "缺貨",
"other": "其他"
},
"Source DO": "原送貨單",
"Source DO Code": "原送貨單編號",
"Source DO code is required": "請輸入原送貨單編號",
"Source DO must be completed": "原送貨單須為已送貨狀態",
"Source DO not found": "找不到原送貨單",
"Submit": "提交",
"Target DO": "目標送貨單",
"This item is already in the draft list": "此物品已在待提交列表中",
@@ -83,6 +98,7 @@
"Truck lane search requires date title": "需選擇預計送貨日期",
"Truck lane search requires date message": "已填寫車線號碼時,請一併選擇預計送貨日期後再搜索。",
"Truck Lance Code": "車線號碼",
"Truck Lane": "車線",
"Select Remark": "選擇備註",
"Confirm Assignment": "確認分配",
"Submit Qty": "提交數量",


Cargando…
Cancelar
Guardar