瀏覽代碼

Merge branch 'production' of https://git.2fi-solutions.com/jason/FPSMS-frontend into production

# Conflicts:
#	src/components/DoSearch/DoReplenishmentTab.tsx
production
tommy 6 天之前
父節點
當前提交
8df6e68854
共有 11 個檔案被更改,包括 1518 行新增666 行删除
  1. +1
    -0
      src/app/(main)/report/ReportSelectionDashboard.tsx
  2. +205
    -0
      src/app/(main)/report/bomShopSyncReportApi.ts
  3. +3
    -0
      src/app/(main)/report/page.tsx
  4. +1
    -1
      src/app/(main)/report/reportCategories.ts
  5. +6
    -0
      src/app/api/do/actions.tsx
  6. +975
    -574
      src/components/DoSearch/DoReplenishmentTab.tsx
  7. +238
    -60
      src/components/DoSearch/ReplenishmentFilterField.tsx
  8. +6
    -6
      src/components/DoSearch/batchReleaseReplenishmentHtml.ts
  9. +23
    -1
      src/config/reportConfig.ts
  10. +30
    -12
      src/i18n/en/do.json
  11. +30
    -12
      src/i18n/zh/do.json

+ 1
- 0
src/app/(main)/report/ReportSelectionDashboard.tsx 查看文件

@@ -32,6 +32,7 @@ const REPORT_ICON_MAP: Record<string, SvgIconComponent> = {
"rep-013": LocalShippingOutlinedIcon, "rep-013": LocalShippingOutlinedIcon,
"rep-006": BarChartOutlinedIcon, "rep-006": BarChartOutlinedIcon,
"rep-005": PieChartOutlineOutlinedIcon, "rep-005": PieChartOutlineOutlinedIcon,
"rep-015": LayersOutlinedIcon,
}; };


const reportById = Object.fromEntries(REPORTS.map((r) => [r.id, r])); const reportById = Object.fromEntries(REPORTS.map((r) => [r.id, r]));


+ 205
- 0
src/app/(main)/report/bomShopSyncReportApi.ts 查看文件

@@ -0,0 +1,205 @@
"use client";

import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import {
exportMultiSheetToXlsx,
} from "@/app/(main)/chart/_components/exportChartToXlsx";

export interface BomShopSyncReportSummary {
totalAttempts?: number;
success?: number;
skippedUnchanged?: number;
failed?: number;
syncDateStart?: string;
syncDateEnd?: string;
}

export interface BomShopSyncRow {
syncLogId?: number;
syncDateTime?: string;
bomId?: number;
bomRoutingCode?: string;
finishedItemCode?: string;
finishedItemName?: string;
m18HeaderCode?: string;
version?: string;
m18RecordId?: number;
syncStatus?: string;
synced?: boolean;
m18ApiStatus?: boolean;
failureReason?: string;
message?: string;
}

export interface BomShopSyncMaterialRow {
syncLogId?: number;
syncDateTime?: string;
bomId?: number;
finishedItemCode?: string;
m18HeaderCode?: string;
version?: string;
syncStatus?: string;
lineNo?: string;
materialName?: string;
udfProductM18Id?: number;
udfBaseUnit?: string;
udfQty?: number;
udfSupplierM18Id?: number;
udfPurchaseUnitM18Id?: number;
}

export interface BomShopSyncReportResponse {
summary?: BomShopSyncReportSummary;
syncRows?: BomShopSyncRow[];
materialRows?: BomShopSyncMaterialRow[];
}

const SHEET_SYNC = "BOM同步記錄";
const SHEET_MATERIALS = "BOM物料明細";

const NO_DATA_NOTE =
"(篩選範圍內無資料 / No records in the selected range)";

/** Column keys for sheet 1 — used for headers when there are no data rows. */
function emptySyncSheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> {
return {
同步時間: note,
成品貨號: "",
成品名稱: "",
BOM路由編號: "",
"M18 BOM Code": "",
版本: "",
"M18 Record Id": "",
狀態: "",
失敗原因: "",
訊息: "",
"BOM Id": "",
"Sync Log Id": "",
};
}

/** Column keys for sheet 2 — used for headers when there are no data rows. */
function emptyMaterialSheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> {
return {
同步時間: note,
成品貨號: "",
"M18 BOM Code": "",
版本: "",
狀態: "",
行號: "",
物料名稱: "",
"M18 Product Id": "",
單位: "",
用量: "",
"M18 Supplier Id": "",
"M18 Purchase Unit Id": "",
"Sync Log Id": "",
};
}

export async function fetchBomShopSyncReportData(
criteria: Record<string, string>,
): Promise<BomShopSyncReportResponse> {
const queryParams = new URLSearchParams(criteria).toString();
const url = `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history?${queryParams}`;

const response = await clientAuthFetch(url, {
method: "GET",
headers: { Accept: "application/json" },
});

if (response.status === 401 || response.status === 403)
throw new Error("Unauthorized");
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);

return (await response.json()) as BomShopSyncReportResponse;
}

function syncStatusLabel(status: string | undefined): string {
switch (status) {
case "SUCCESS":
return "成功";
case "SKIPPED_UNCHANGED":
return "略過(內容未變)";
case "FAILED":
return "失敗";
default:
return status ?? "";
}
}

function toSyncExcelRow(r: BomShopSyncRow): Record<string, unknown> {
const base = emptySyncSheetRow("");
return {
...base,
同步時間: r.syncDateTime ?? "",
成品貨號: r.finishedItemCode ?? "",
成品名稱: r.finishedItemName ?? "",
BOM路由編號: r.bomRoutingCode ?? "",
"M18 BOM Code": r.m18HeaderCode ?? "",
版本: r.version ?? "",
"M18 Record Id": r.m18RecordId ?? "",
狀態: syncStatusLabel(r.syncStatus),
失敗原因: r.failureReason ?? "",
訊息: r.message ?? "",
"BOM Id": r.bomId ?? "",
"Sync Log Id": r.syncLogId ?? "",
};
}

function toMaterialExcelRow(r: BomShopSyncMaterialRow): Record<string, unknown> {
const base = emptyMaterialSheetRow("");
return {
...base,
同步時間: r.syncDateTime ?? "",
成品貨號: r.finishedItemCode ?? "",
"M18 BOM Code": r.m18HeaderCode ?? "",
版本: r.version ?? "",
狀態: syncStatusLabel(r.syncStatus),
行號: r.lineNo ?? "",
物料名稱: r.materialName ?? "",
"M18 Product Id": r.udfProductM18Id ?? "",
單位: r.udfBaseUnit ?? "",
用量: r.udfQty ?? "",
"M18 Supplier Id": r.udfSupplierM18Id ?? "",
"M18 Purchase Unit Id": r.udfPurchaseUnitM18Id ?? "",
"Sync Log Id": r.syncLogId ?? "",
};
}

export async function generateBomShopSyncReportExcel(
criteria: Record<string, string>,
reportTitle: string = "M18 BOM Shop 同步記錄",
): Promise<void> {
const data = await fetchBomShopSyncReportData(criteria);
const syncRows =
(data.syncRows ?? []).length > 0
? (data.syncRows ?? []).map(toSyncExcelRow)
: [emptySyncSheetRow()];
const materialRows =
(data.materialRows ?? []).length > 0
? (data.materialRows ?? []).map(toMaterialExcelRow)
: [emptyMaterialSheetRow()];

const start = criteria.syncDateStart;
const end = criteria.syncDateEnd;
let datePart: string;
if (start && end && start === end) {
datePart = start;
} else if (start || end) {
datePart = `${start || ""}_to_${end || ""}`;
} else {
datePart = new Date().toISOString().slice(0, 10);
}
const filename = `${reportTitle}_${datePart.replace(/[^\d\-_/]/g, "")}`;

exportMultiSheetToXlsx(
[
{ name: SHEET_SYNC, rows: syncRows },
{ name: SHEET_MATERIALS, rows: materialRows },
],
filename,
);
}

+ 3
- 0
src/app/(main)/report/page.tsx 查看文件

@@ -30,6 +30,7 @@ import {
fetchSemiFGItemCodesWithCategory fetchSemiFGItemCodesWithCategory
} from './semiFGProductionAnalysisApi'; } from './semiFGProductionAnalysisApi';
import { generateGrnReportExcel } from './grnReportApi'; import { generateGrnReportExcel } from './grnReportApi';
import { generateBomShopSyncReportExcel } from './bomShopSyncReportApi';
import { import {
FEATURE_USAGE, FEATURE_USAGE,
FEATURE_USAGE_ACTION, FEATURE_USAGE_ACTION,
@@ -261,6 +262,8 @@ export default function ReportPage() {
currentReport.title, currentReport.title,
includeGrnFinancialColumns includeGrnFinancialColumns
); );
} else if (currentReport.id === 'rep-015') {
await generateBomShopSyncReportExcel(criteria, currentReport.title);
} else { } else {
// Backend returns actual .xlsx bytes for this Excel endpoint. // Backend returns actual .xlsx bytes for this Excel endpoint.
const queryParams = const queryParams =


+ 1
- 1
src/app/(main)/report/reportCategories.ts 查看文件

@@ -33,6 +33,6 @@ export const REPORT_CATEGORIES: ReportCategoryConfig[] = [
headerBg: "#f5d4a8", headerBg: "#f5d4a8",
bodyBg: "#fdf6ec", bodyBg: "#fdf6ec",
accent: "#e65100", accent: "#e65100",
reportIds: ["rep-006", "rep-005"],
reportIds: ["rep-006", "rep-005", "rep-015"],
}, },
]; ];

+ 6
- 0
src/app/api/do/actions.tsx 查看文件

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


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




+ 975
- 574
src/components/DoSearch/DoReplenishmentTab.tsx
文件差異過大導致無法顯示
查看文件


+ 238
- 60
src/components/DoSearch/ReplenishmentFilterField.tsx 查看文件

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


export const REPLENISHMENT_FIELD_ICON_SX = (theme: Theme) => ({ export const REPLENISHMENT_FIELD_ICON_SX = (theme: Theme) => ({
@@ -28,6 +28,7 @@ export const REPLENISHMENT_TEXTFIELD_SX = (theme: Theme) =>
borderRadius: 2, borderRadius: 2,
bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white",
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
}, },
"& .MuiFilledInput-root.Mui-focused": { "& .MuiFilledInput-root.Mui-focused": {
bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white", bgcolor: theme.palette.mode === "dark" ? "grey.800" : "common.white",
@@ -56,6 +57,7 @@ export const REPLENISHMENT_AUTOCOMPLETE_SX = (theme: Theme) =>
width: "100%", width: "100%",
}, },
"& .MuiAutocomplete-inputRoot": { "& .MuiAutocomplete-inputRoot": {
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
paddingTop: `${REPLENISHMENT_FIELD_BODY_PY} !important`, paddingTop: `${REPLENISHMENT_FIELD_BODY_PY} !important`,
paddingBottom: `${REPLENISHMENT_FIELD_BODY_PY} !important`, paddingBottom: `${REPLENISHMENT_FIELD_BODY_PY} !important`,
paddingLeft: `${theme.spacing(REPLENISHMENT_FIELD_BODY_PX)} !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). */ /** Horizontal padding aligned with MUI filled input (spacing 1.5 = 12px). */
export const REPLENISHMENT_FIELD_BODY_PX = 1.5; 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. */ /** Source DO summary header: same inset as textbox content area. */
export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) => export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) =>
({ ({
@@ -89,7 +160,7 @@ export const REPLENISHMENT_SOURCE_HEADER_SX = (theme: Theme) =>
}) as const; }) as const;


type ReplenishmentFieldLabelProps = { type ReplenishmentFieldLabelProps = {
icon: ReactNode;
icon?: ReactNode;
title: string; title: string;
required?: boolean; required?: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
@@ -102,14 +173,22 @@ export function ReplenishmentFieldLabel({
sx, sx,
}: ReplenishmentFieldLabelProps) { }: ReplenishmentFieldLabelProps) {
return ( 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} {title}
{required ? ( {required ? (
<Typography component="span" color="error.main" aria-hidden="true">
<Box component="span" color="error.main" aria-hidden="true">
{" *"} {" *"}
</Typography>
</Box>
) : null} ) : null}
</Typography> </Typography>
</Stack> </Stack>
@@ -143,16 +222,33 @@ export function ReplenishmentTextField(props: ReplenishmentTextFieldProps) {
size="small" size="small"
fullWidth fullWidth
variant="filled" variant="filled"
sx={(theme) => ({
...REPLENISHMENT_TEXTFIELD_SX(theme),
...(typeof sx === "function" ? sx(theme) : sx),
})}
sx={[REPLENISHMENT_TEXTFIELD_SX, sx] as SxProps<Theme>}
InputProps={{ disableUnderline: true, ...InputProps }} InputProps={{ disableUnderline: true, ...InputProps }}
{...rest} {...rest}
/> />
); );
} }


/** 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" },
...REPLENISHMENT_FIELD_CONTROL_ROOT_SX,
},
"& .MuiSelect-select": {
paddingTop: REPLENISHMENT_FIELD_BODY_PY,
paddingBottom: REPLENISHMENT_FIELD_BODY_PY,
display: "flex",
alignItems: "center",
},
}) 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,
@@ -170,14 +266,18 @@ export function ReplenishmentItemEntryPlainText({
return ( return (
<Box <Box
component="span" component="span"
sx={(theme) => ({
display: "block",
color: theme.palette.text.primary,
wordBreak: "break-word",
minWidth: 0,
minHeight: reserveSpace ? theme.spacing(5) : undefined,
...(typeof sx === "function" ? sx(theme) : sx),
})}
sx={
[
(theme) => ({
display: "block",
color: theme.palette.text.primary,
wordBreak: "break-word",
minWidth: 0,
minHeight: reserveSpace ? theme.spacing(5) : undefined,
}),
sx,
] as SxProps<Theme>
}
> >
{isEmpty ? "\u00A0" : value} {isEmpty ? "\u00A0" : value}
</Box> </Box>
@@ -225,17 +325,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 = { export const REPLENISHMENT_TABLE_SX = {
tableLayout: { md: "fixed" },
width: "100%", width: "100%",
tableLayout: "fixed",
"& .MuiTableCell-root": { "& .MuiTableCell-root": {
typography: "body2", typography: "body2",
borderColor: "divider", borderColor: "divider",
py: 1.25,
px: 2,
py: 1,
px: 1.25,
}, },
"& .MuiTableCell-root:first-of-type": { "& .MuiTableCell-root:first-of-type": {
pl: 3.5,
pl: 1.5,
}, },
"& .MuiTableHead-root .MuiTableCell-root": { "& .MuiTableHead-root .MuiTableCell-root": {
fontWeight: 600, fontWeight: 600,
@@ -361,10 +501,27 @@ export const REPLENISHMENT_TABLE_ACTION_CELL_INNER_SX = {
width: "100%", width: "100%",
} as const; } 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) => ({ export const replenishmentSearchGridLabelSx = (col: number) => ({
gridColumn: { xs: 1, lg: col }, gridColumn: { xs: 1, lg: col },
gridRow: { xs: "auto", lg: 1 }, gridRow: { xs: "auto", lg: 1 },
minWidth: 0,
minWidth: "min-content",
overflow: "hidden",
textOverflow: "ellipsis",
}); });


export const replenishmentSearchGridInputSx = (col: number) => ({ export const replenishmentSearchGridInputSx = (col: number) => ({
@@ -378,53 +535,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 }, gridRow: { xs: "auto", lg: 2 },
minWidth: 0,
display: "flex", display: "flex",
justifyContent: { xs: "stretch", lg: "flex-start" },
alignItems: "stretch", 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) => ({ export const REPLENISHMENT_LOOKUP_BUTTON_TEXT_SX = (theme: Theme) => ({
fontSize: theme.typography.body2.fontSize, fontSize: theme.typography.body2.fontSize,
fontWeight: 600, fontWeight: 600,
lineHeight: 1, 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: "100%", 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_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;



+ 6
- 6
src/components/DoSearch/batchReleaseReplenishmentHtml.ts 查看文件

@@ -61,16 +61,16 @@ export function deriveReplenishmentFetchParams(
}; };
} }


const shopTokens = [
...new Set(dosForRelease.map(shopTokenFromDoRow).filter(Boolean)),
];
const trucks = [
...new Set(
const shopTokens = Array.from(
new Set(dosForRelease.map(shopTokenFromDoRow).filter(Boolean)),
);
const trucks = Array.from(
new Set(
dosForRelease dosForRelease
.map((row) => row.truckLanceCode?.trim()) .map((row) => row.truckLanceCode?.trim())
.filter((value): value is string => Boolean(value)), .filter((value): value is string => Boolean(value)),
), ),
];
);


if (shopTokens.length === 1 && trucks.length === 1) { if (shopTokens.length === 1 && trucks.length === 1) {
return { shopName: shopTokens[0], truckLaneCode: trucks[0] }; return { shopName: shopTokens[0], truckLaneCode: trucks[0] };


+ 23
- 1
src/config/reportConfig.ts 查看文件

@@ -290,5 +290,27 @@ export const REPORTS: ReportDefinition[] = [
dynamicOptionsParam: "stockCategory", dynamicOptionsParam: "stockCategory",
options: [] }, options: [] },
] ]
}
},
{
id: "rep-015",
title: "M18 BOM Shop 同步記錄",
apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history`,
responseType: "excel",
fields: [
{ label: "同步日期:由 Sync Date Start", name: "syncDateStart", type: "date", required: false },
{ label: "同步日期:至 Sync Date End", name: "syncDateEnd", type: "date", required: false },
{ label: "成品貨號 Finished Item Code", name: "finishedItemCode", type: "text", required: false },
{
label: "同步狀態 Sync Status",
name: "syncStatus",
type: "select",
required: false,
options: [
{ label: "全部 All", value: "all" },
{ label: "成功 Success", value: "success" },
{ label: "失敗 Failed", value: "failed" },
],
},
],
},
] ]

+ 30
- 12
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",
@@ -71,27 +73,35 @@
"Delivery Date": "Delivery Date", "Delivery Date": "Delivery Date",
"Enter last 4 characters of DO code": "Enter last 4 characters of DO code", "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", "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.", "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", "Shop code or name is required": "Shop code or name is required",
"Draft List": "Draft List", "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", "Clear": "Clear",
"Enter item code to search": "Enter item code to search", "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", "Item": "Item",
"Lookup": "Lookup",
"Lookup": "Match DO",
"No draft rows to submit": "No draft rows to submit", "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", "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", "Picker Name": "Picker Name",
"Please select an item": "Please select an item", "Please select an item": "Please select an item",
"Records saved locally for preview. Backend integration pending.": "Records saved locally for preview. Backend integration pending.", "Records saved locally for preview. Backend integration pending.": "Records saved locally for preview. Backend integration pending.",
"Replenishment Code": "Replenishment No.", "Replenishment Code": "Replenishment No.",
"Ref Code": "Ref Code", "Ref Code": "Ref Code",
"Replenish Qty": "Replenish Qty", "Replenish Qty": "Replenish Qty",
"Replenish Qty short": "Replenish",
"Replenish qty must be greater than zero": "Replenish qty must be greater than zero", "Replenish qty must be greater than zero": "Replenish qty must be greater than zero",
"Replenishment": "Replenishment", "Replenishment": "Replenishment",
"Delivery date is required": "Delivery date is required", "Delivery date is required": "Delivery date is required",
@@ -110,11 +120,18 @@
"replenishmentDatePlaceholder": "YYYY-MM-DD", "replenishmentDatePlaceholder": "YYYY-MM-DD",
"replenishmentDoSuffixPlaceholder": "DO No. (last 4)", "replenishmentDoSuffixPlaceholder": "DO No. (last 4)",
"replenishmentShopPlaceholder": "Shop Code", "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", "Submit": "Submit",
"Target DO": "Target DO", "Target DO": "Target DO",
"This item is already in the draft list": "This item is already in the draft list", "This item is already in the draft list": "This item is already in the draft list",
@@ -143,6 +160,7 @@
"Supplier Name": "Supplier Name", "Supplier Name": "Supplier Name",
"Truck Availability Warning": "Truck Availability Warning", "Truck Availability Warning": "Truck Availability Warning",
"Truck Lance Code": "Truck Lance Code", "Truck Lance Code": "Truck Lance Code",
"Truck Lane": "Truck Lane",
"Truck X": "Truck X", "Truck X": "Truck X",
"Truck lane search requires date message": "Truck lane search requires date message", "Truck lane search requires date message": "Truck lane search requires date message",
"Truck lane search requires date title": "Truck lane search requires date title", "Truck lane search requires date title": "Truck lane search requires date title",


+ 30
- 12
src/i18n/zh/do.json 查看文件

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


Loading…
取消
儲存