# Conflicts: # src/components/DoSearch/DoReplenishmentTab.tsxproduction
| @@ -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])); | ||||
| @@ -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, | |||||
| ); | |||||
| } | |||||
| @@ -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 = | ||||
| @@ -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"], | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -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; | ||||
| } | } | ||||
| @@ -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; | |||||
| @@ -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] }; | ||||
| @@ -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" }, | |||||
| ], | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ] | ] | ||||
| @@ -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", | ||||
| @@ -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": "提交數量", | ||||