| @@ -1,6 +1,7 @@ | |||
| "use client"; | |||
| import React from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| FormHelperText, | |||
| Grid, | |||
| @@ -26,14 +27,19 @@ export default function ItemQcReportFilters({ | |||
| criteria, | |||
| onFieldChange, | |||
| }: ItemQcReportFiltersProps) { | |||
| const { t } = useTranslation("report"); | |||
| const qcItemFilter = (criteria.qcItemScope || "all") as QcItemFilter; | |||
| const field = (name: string, fallback: string) => | |||
| t(`reports.rep-010.fields.${name}`, { defaultValue: fallback }); | |||
| const opt = (fieldName: string, value: string, fallback: string) => | |||
| t(`reports.rep-010.options.${fieldName}.${value}`, { defaultValue: fallback }); | |||
| return ( | |||
| <Grid container spacing={3}> | |||
| <Grid item {...gridSize}> | |||
| <TextField | |||
| fullWidth | |||
| label="QC 檢測日期:由 QC Date Start" | |||
| label={field("lastInDateStart", "QC 檢測日期:由 QC Date Start")} | |||
| type="date" | |||
| value={criteria.lastInDateStart || ""} | |||
| onChange={(e) => onFieldChange("lastInDateStart", e.target.value)} | |||
| @@ -43,7 +49,7 @@ export default function ItemQcReportFilters({ | |||
| <Grid item {...gridSize}> | |||
| <TextField | |||
| fullWidth | |||
| label="QC 檢測日期:至 QC Date End" | |||
| label={field("lastInDateEnd", "QC 檢測日期:至 QC Date End")} | |||
| type="date" | |||
| value={criteria.lastInDateEnd || ""} | |||
| onChange={(e) => onFieldChange("lastInDateEnd", e.target.value)} | |||
| @@ -54,19 +60,19 @@ export default function ItemQcReportFilters({ | |||
| <TextField | |||
| fullWidth | |||
| select | |||
| label="QC 類型" | |||
| label={field("qcType", "QC 類型")} | |||
| value={criteria.qcType || "all"} | |||
| onChange={(e) => onFieldChange("qcType", e.target.value)} | |||
| > | |||
| <MenuItem value="all">全部</MenuItem> | |||
| <MenuItem value="IQC">IQC</MenuItem> | |||
| <MenuItem value="EPQC">EPQC</MenuItem> | |||
| <MenuItem value="all">{opt("qcType", "all", "全部")}</MenuItem> | |||
| <MenuItem value="IQC">{opt("qcType", "IQC", "IQC(採購)")}</MenuItem> | |||
| <MenuItem value="EPQC">{opt("qcType", "EPQC", "EPQC(工單)")}</MenuItem> | |||
| </TextField> | |||
| </Grid> | |||
| <Grid item {...gridSize}> | |||
| <TextField | |||
| fullWidth | |||
| label="貨品編號 Item Code" | |||
| label={field("itemCode", "貨品編號 Item Code")} | |||
| value={criteria.itemCode || ""} | |||
| onChange={(e) => onFieldChange("itemCode", e.target.value)} | |||
| placeholder="e.g. MJ0364" | |||
| @@ -76,17 +82,17 @@ export default function ItemQcReportFilters({ | |||
| <TextField | |||
| fullWidth | |||
| select | |||
| label="QC 項目範圍" | |||
| label={field("qcItemScope", "QC 項目範圍")} | |||
| value={qcItemFilter === "measurable" ? "measurable" : "all"} | |||
| onChange={(e) => onFieldChange("qcItemScope", e.target.value)} | |||
| > | |||
| <MenuItem value="all">全部 QC 項目</MenuItem> | |||
| <MenuItem value="measurable">只包含溫度濕度</MenuItem> | |||
| <MenuItem value="all">{opt("qcItemScope", "all", "全部 QC 項目")}</MenuItem> | |||
| <MenuItem value="measurable">{opt("qcItemScope", "measurable", "只包含溫度濕度")}</MenuItem> | |||
| </TextField> | |||
| <FormHelperText> | |||
| {qcItemFilter === "measurable" | |||
| ? "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。" | |||
| : "匯出全部 QC 檢驗項目(含溫度/濕度)。"} | |||
| ? t("qcScopeHelpMeasurable") | |||
| : t("qcScopeHelpAll")} | |||
| </FormHelperText> | |||
| </Grid> | |||
| </Grid> | |||
| @@ -19,6 +19,7 @@ import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutl | |||
| import type { SvgIconComponent } from "@mui/icons-material"; | |||
| import { REPORTS } from "@/config/reportConfig"; | |||
| import { REPORT_CATEGORIES, type ReportCategoryConfig } from "./reportCategories"; | |||
| import { useReportLabels } from "./reportI18n"; | |||
| const REPORT_ICON_MAP: Record<string, SvgIconComponent> = { | |||
| "rep-011": Inventory2OutlinedIcon, | |||
| @@ -116,6 +117,7 @@ function CategoryColumn({ | |||
| selectedReportId: string; | |||
| onSelectReport: (reportId: string) => void; | |||
| }) { | |||
| const { reportTitle, categoryTitle } = useReportLabels(); | |||
| const reports = category.reportIds | |||
| .map((id) => reportById[id]) | |||
| .filter(Boolean); | |||
| @@ -140,7 +142,7 @@ function CategoryColumn({ | |||
| }} | |||
| > | |||
| <Typography variant="subtitle1" fontWeight="bold"> | |||
| {category.title} | |||
| {categoryTitle(category.id, category.title)} | |||
| </Typography> | |||
| </Box> | |||
| <Box | |||
| @@ -155,7 +157,7 @@ function CategoryColumn({ | |||
| <Grid item xs={6} key={report.id}> | |||
| <ReportCard | |||
| reportId={report.id} | |||
| title={report.title} | |||
| title={reportTitle(report)} | |||
| accent={category.accent} | |||
| selected={selectedReportId === report.id} | |||
| onClick={() => onSelectReport(report.id)} | |||
| @@ -1,6 +1,7 @@ | |||
| "use client"; | |||
| import React, { useState, useEffect } from 'react'; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| Dialog, | |||
| DialogTitle, | |||
| @@ -43,6 +44,7 @@ export default function SemiFGProductionAnalysisReport({ | |||
| reportTitle = '成品/半成品生產分析報告', | |||
| onExportSuccess, | |||
| }: SemiFGProductionAnalysisReportProps) { | |||
| const { t } = useTranslation("report"); | |||
| const [showConfirmDialog, setShowConfirmDialog] = useState(false); | |||
| const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]); | |||
| const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({}); | |||
| @@ -70,7 +72,7 @@ export default function SemiFGProductionAnalysisReport({ | |||
| setExportFormat(format); | |||
| // Validate required fields | |||
| if (requiredFieldLabels.length > 0) { | |||
| alert(`缺少必填條件:\n- ${requiredFieldLabels.join('\n- ')}`); | |||
| alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') })); | |||
| return; | |||
| } | |||
| @@ -107,7 +109,7 @@ export default function SemiFGProductionAnalysisReport({ | |||
| setShowConfirmDialog(false); | |||
| } catch (error) { | |||
| console.error('Failed to generate report:', error); | |||
| alert('An error occurred while generating the report. Please try again.'); | |||
| alert(t('generateError')); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| @@ -124,7 +126,7 @@ export default function SemiFGProductionAnalysisReport({ | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? '生成 PDF...' : '下載報告 (PDF)'} | |||
| {loading ? t('generatingPdf') : t('downloadPdf')} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -134,7 +136,7 @@ export default function SemiFGProductionAnalysisReport({ | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? '生成 Excel...' : '下載報告 (Excel)'} | |||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||
| </Button> | |||
| </div> | |||
| @@ -147,22 +149,22 @@ export default function SemiFGProductionAnalysisReport({ | |||
| > | |||
| <DialogTitle> | |||
| <Typography variant="h6" fontWeight="bold"> | |||
| 已選擇的物料編號以及列印成品/半成品生產分析報告 | |||
| {t('semiFgConfirmTitle')} | |||
| </Typography> | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| 請確認以下已選擇的物料編號及其類別: | |||
| {t('semiFgConfirmHint')} | |||
| </Typography> | |||
| <TableContainer component={Paper} variant="outlined"> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell> | |||
| <strong>物料編號及名稱</strong> | |||
| <strong>{t('semiFgColItem')}</strong> | |||
| </TableCell> | |||
| <TableCell> | |||
| <strong>類別</strong> | |||
| <strong>{t('semiFgColCategory')}</strong> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| @@ -187,7 +189,7 @@ export default function SemiFGProductionAnalysisReport({ | |||
| </TableContainer> | |||
| </DialogContent> | |||
| <DialogActions sx={{ p: 2 }}> | |||
| <Button onClick={() => setShowConfirmDialog(false)}>取消</Button> | |||
| <Button onClick={() => setShowConfirmDialog(false)}>{t('cancel')}</Button> | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => executeExport()} | |||
| @@ -196,11 +198,11 @@ export default function SemiFGProductionAnalysisReport({ | |||
| > | |||
| {loading | |||
| ? exportFormat === 'excel' | |||
| ? '生成 Excel...' | |||
| : '生成 PDF...' | |||
| ? t('generatingExcel') | |||
| : t('generatingPdf') | |||
| : exportFormat === 'excel' | |||
| ? '確認下載 Excel' | |||
| : '確認下載 PDF'} | |||
| ? t('confirmDownloadExcel') | |||
| : t('confirmDownloadPdf')} | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| @@ -5,6 +5,8 @@ import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| import { | |||
| exportMultiSheetToXlsx, | |||
| } from "@/app/(main)/chart/_components/exportChartToXlsx"; | |||
| import { reportExcelT as tx } from "./reportI18n"; | |||
| import type { TFunction } from "i18next"; | |||
| export interface BomShopSyncReportSummary { | |||
| totalAttempts?: number; | |||
| @@ -55,43 +57,64 @@ export interface BomShopSyncReportResponse { | |||
| materialRows?: BomShopSyncMaterialRow[]; | |||
| } | |||
| const SHEET_SYNC = "BOM同步記錄"; | |||
| const SHEET_MATERIALS = "BOM物料明細"; | |||
| const NO_DATA_NOTE = | |||
| "(篩選範圍內無資料 / No records in the selected range)"; | |||
| function bomSyncLabels(t?: TFunction) { | |||
| return { | |||
| sheetSync: tx(t, "excel.bomSync.sheetSync", "BOM同步記錄"), | |||
| sheetMaterials: tx(t, "excel.bomSync.sheetMaterials", "BOM物料明細"), | |||
| noData: tx(t, "excel.noData", "(篩選範圍內無資料)"), | |||
| syncTime: tx(t, "excel.bomSync.syncTime", "同步時間"), | |||
| finishedItemCode: tx(t, "excel.bomSync.finishedItemCode", "成品貨號"), | |||
| finishedItemName: tx(t, "excel.bomSync.finishedItemName", "成品名稱"), | |||
| bomRoutingCode: tx(t, "excel.bomSync.bomRoutingCode", "BOM路由編號"), | |||
| version: tx(t, "excel.bomSync.version", "版本"), | |||
| status: tx(t, "excel.bomSync.status", "狀態"), | |||
| failureReason: tx(t, "excel.bomSync.failureReason", "失敗原因"), | |||
| message: tx(t, "excel.bomSync.message", "訊息"), | |||
| lineNo: tx(t, "excel.bomSync.lineNo", "行號"), | |||
| materialName: tx(t, "excel.bomSync.materialName", "物料名稱"), | |||
| uom: tx(t, "excel.bomSync.uom", "單位"), | |||
| qty: tx(t, "excel.bomSync.qty", "用量"), | |||
| statusSuccess: tx(t, "excel.bomSync.statusSuccess", "成功"), | |||
| statusSkipped: tx(t, "excel.bomSync.statusSkipped", "略過(內容未變)"), | |||
| statusFailed: tx(t, "excel.bomSync.statusFailed", "失敗"), | |||
| }; | |||
| } | |||
| /** Column keys for sheet 1 — used for headers when there are no data rows. */ | |||
| function emptySyncSheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> { | |||
| function emptySyncSheetRow( | |||
| L: ReturnType<typeof bomSyncLabels>, | |||
| note?: string, | |||
| ): Record<string, unknown> { | |||
| return { | |||
| 同步時間: note, | |||
| 成品貨號: "", | |||
| 成品名稱: "", | |||
| BOM路由編號: "", | |||
| [L.syncTime]: note ?? L.noData, | |||
| [L.finishedItemCode]: "", | |||
| [L.finishedItemName]: "", | |||
| [L.bomRoutingCode]: "", | |||
| "M18 BOM Code": "", | |||
| 版本: "", | |||
| [L.version]: "", | |||
| "M18 Record Id": "", | |||
| 狀態: "", | |||
| 失敗原因: "", | |||
| 訊息: "", | |||
| [L.status]: "", | |||
| [L.failureReason]: "", | |||
| [L.message]: "", | |||
| "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> { | |||
| function emptyMaterialSheetRow( | |||
| L: ReturnType<typeof bomSyncLabels>, | |||
| note?: string, | |||
| ): Record<string, unknown> { | |||
| return { | |||
| 同步時間: note, | |||
| 成品貨號: "", | |||
| [L.syncTime]: note ?? L.noData, | |||
| [L.finishedItemCode]: "", | |||
| "M18 BOM Code": "", | |||
| 版本: "", | |||
| 狀態: "", | |||
| 行號: "", | |||
| 物料名稱: "", | |||
| [L.version]: "", | |||
| [L.status]: "", | |||
| [L.lineNo]: "", | |||
| [L.materialName]: "", | |||
| "M18 Product Id": "", | |||
| 單位: "", | |||
| 用量: "", | |||
| [L.uom]: "", | |||
| [L.qty]: "", | |||
| "M18 Supplier Id": "", | |||
| "M18 Purchase Unit Id": "", | |||
| "Sync Log Id": "", | |||
| @@ -117,52 +140,59 @@ export async function fetchBomShopSyncReportData( | |||
| return (await response.json()) as BomShopSyncReportResponse; | |||
| } | |||
| function syncStatusLabel(status: string | undefined): string { | |||
| function syncStatusLabel( | |||
| status: string | undefined, | |||
| L: ReturnType<typeof bomSyncLabels>, | |||
| ): string { | |||
| switch (status) { | |||
| case "SUCCESS": | |||
| return "成功"; | |||
| return L.statusSuccess; | |||
| case "SKIPPED_UNCHANGED": | |||
| return "略過(內容未變)"; | |||
| return L.statusSkipped; | |||
| case "FAILED": | |||
| return "失敗"; | |||
| return L.statusFailed; | |||
| default: | |||
| return status ?? ""; | |||
| } | |||
| } | |||
| function toSyncExcelRow(r: BomShopSyncRow): Record<string, unknown> { | |||
| const base = emptySyncSheetRow(""); | |||
| function toSyncExcelRow( | |||
| r: BomShopSyncRow, | |||
| L: ReturnType<typeof bomSyncLabels>, | |||
| ): Record<string, unknown> { | |||
| return { | |||
| ...base, | |||
| 同步時間: r.syncDateTime ?? "", | |||
| 成品貨號: r.finishedItemCode ?? "", | |||
| 成品名稱: r.finishedItemName ?? "", | |||
| BOM路由編號: r.bomRoutingCode ?? "", | |||
| ...emptySyncSheetRow(L, ""), | |||
| [L.syncTime]: r.syncDateTime ?? "", | |||
| [L.finishedItemCode]: r.finishedItemCode ?? "", | |||
| [L.finishedItemName]: r.finishedItemName ?? "", | |||
| [L.bomRoutingCode]: r.bomRoutingCode ?? "", | |||
| "M18 BOM Code": r.m18HeaderCode ?? "", | |||
| 版本: r.version ?? "", | |||
| [L.version]: r.version ?? "", | |||
| "M18 Record Id": r.m18RecordId ?? "", | |||
| 狀態: syncStatusLabel(r.syncStatus), | |||
| 失敗原因: r.failureReason ?? "", | |||
| 訊息: r.message ?? "", | |||
| [L.status]: syncStatusLabel(r.syncStatus, L), | |||
| [L.failureReason]: r.failureReason ?? "", | |||
| [L.message]: r.message ?? "", | |||
| "BOM Id": r.bomId ?? "", | |||
| "Sync Log Id": r.syncLogId ?? "", | |||
| }; | |||
| } | |||
| function toMaterialExcelRow(r: BomShopSyncMaterialRow): Record<string, unknown> { | |||
| const base = emptyMaterialSheetRow(""); | |||
| function toMaterialExcelRow( | |||
| r: BomShopSyncMaterialRow, | |||
| L: ReturnType<typeof bomSyncLabels>, | |||
| ): Record<string, unknown> { | |||
| return { | |||
| ...base, | |||
| 同步時間: r.syncDateTime ?? "", | |||
| 成品貨號: r.finishedItemCode ?? "", | |||
| ...emptyMaterialSheetRow(L, ""), | |||
| [L.syncTime]: r.syncDateTime ?? "", | |||
| [L.finishedItemCode]: r.finishedItemCode ?? "", | |||
| "M18 BOM Code": r.m18HeaderCode ?? "", | |||
| 版本: r.version ?? "", | |||
| 狀態: syncStatusLabel(r.syncStatus), | |||
| 行號: r.lineNo ?? "", | |||
| 物料名稱: r.materialName ?? "", | |||
| [L.version]: r.version ?? "", | |||
| [L.status]: syncStatusLabel(r.syncStatus, L), | |||
| [L.lineNo]: r.lineNo ?? "", | |||
| [L.materialName]: r.materialName ?? "", | |||
| "M18 Product Id": r.udfProductM18Id ?? "", | |||
| 單位: r.udfBaseUnit ?? "", | |||
| 用量: r.udfQty ?? "", | |||
| [L.uom]: r.udfBaseUnit ?? "", | |||
| [L.qty]: r.udfQty ?? "", | |||
| "M18 Supplier Id": r.udfSupplierM18Id ?? "", | |||
| "M18 Purchase Unit Id": r.udfPurchaseUnitM18Id ?? "", | |||
| "Sync Log Id": r.syncLogId ?? "", | |||
| @@ -172,16 +202,18 @@ function toMaterialExcelRow(r: BomShopSyncMaterialRow): Record<string, unknown> | |||
| export async function generateBomShopSyncReportExcel( | |||
| criteria: Record<string, string>, | |||
| reportTitle: string = "M18 BOM Shop 同步記錄", | |||
| t?: TFunction, | |||
| ): Promise<void> { | |||
| const L = bomSyncLabels(t); | |||
| const data = await fetchBomShopSyncReportData(criteria); | |||
| const syncRows = | |||
| (data.syncRows ?? []).length > 0 | |||
| ? (data.syncRows ?? []).map(toSyncExcelRow) | |||
| : [emptySyncSheetRow()]; | |||
| ? (data.syncRows ?? []).map((r) => toSyncExcelRow(r, L)) | |||
| : [emptySyncSheetRow(L)]; | |||
| const materialRows = | |||
| (data.materialRows ?? []).length > 0 | |||
| ? (data.materialRows ?? []).map(toMaterialExcelRow) | |||
| : [emptyMaterialSheetRow()]; | |||
| ? (data.materialRows ?? []).map((r) => toMaterialExcelRow(r, L)) | |||
| : [emptyMaterialSheetRow(L)]; | |||
| const start = criteria.syncDateStart; | |||
| const end = criteria.syncDateEnd; | |||
| @@ -197,8 +229,8 @@ export async function generateBomShopSyncReportExcel( | |||
| exportMultiSheetToXlsx( | |||
| [ | |||
| { name: SHEET_SYNC, rows: syncRows }, | |||
| { name: SHEET_MATERIALS, rows: materialRows }, | |||
| { name: L.sheetSync, rows: syncRows }, | |||
| { name: L.sheetMaterials, rows: materialRows }, | |||
| ], | |||
| filename, | |||
| ); | |||
| @@ -6,6 +6,8 @@ import { | |||
| exportChartToXlsx, | |||
| exportMultiSheetToXlsx, | |||
| } from "@/app/(main)/chart/_components/exportChartToXlsx"; | |||
| import { reportExcelT as tx } from "./reportI18n"; | |||
| import type { TFunction } from "i18next"; | |||
| export interface GrnReportRow { | |||
| poCode?: string; | |||
| @@ -122,77 +124,102 @@ const formatQty = (n: number | undefined | null): string => { | |||
| }).format(Number(n)); | |||
| }; | |||
| /** Excel column headers (bilingual) for GRN report */ | |||
| function grnLabels(t?: TFunction) { | |||
| return { | |||
| sheetDetail: tx(t, "excel.grn.sheetDetail", "PO入倉記錄"), | |||
| sheetListedPo: tx(t, "excel.grn.sheetListedPo", "已上架PO金額"), | |||
| poNo: tx(t, "excel.grn.poNo", "訂單編號"), | |||
| deliveryNoteNo: tx(t, "excel.grn.deliveryNoteNo", "送貨單編號"), | |||
| receiptDate: tx(t, "excel.grn.receiptDate", "收貨日期"), | |||
| itemCode: tx(t, "excel.grn.itemCode", "物料編號"), | |||
| itemName: tx(t, "excel.grn.itemName", "物料名稱"), | |||
| qty: tx(t, "excel.grn.qty", "數量"), | |||
| demandQty: tx(t, "excel.grn.demandQty", "訂單數量"), | |||
| uom: tx(t, "excel.grn.uom", "單位"), | |||
| supplierLotNo: tx(t, "excel.grn.supplierLotNo", "供應商批次"), | |||
| expiryDate: tx(t, "excel.grn.expiryDate", "到期日"), | |||
| supplierCode: tx(t, "excel.grn.supplierCode", "供應商編號"), | |||
| supplier: tx(t, "excel.grn.supplier", "供應商"), | |||
| status: tx(t, "excel.grn.status", "入倉狀態"), | |||
| unitPrice: tx(t, "excel.grn.unitPrice", "單價"), | |||
| currency: tx(t, "excel.grn.currency", "貨幣"), | |||
| amount: tx(t, "excel.grn.amount", "金額"), | |||
| grnCode: tx(t, "excel.grn.grnCode", "M18 入倉單號"), | |||
| grnId: tx(t, "excel.grn.grnId", "M18 記錄編號"), | |||
| poCreator: tx(t, "excel.grn.poCreator", "PO建立者(M18)"), | |||
| note: tx(t, "excel.grn.note", "備註"), | |||
| category: tx(t, "excel.grn.category", "類別"), | |||
| totalAmount: tx(t, "excel.grn.totalAmount", "金額"), | |||
| grnCodes: tx(t, "excel.grn.grnCodes", "M18 入倉單號"), | |||
| noCompletedPo: tx(t, "excel.grn.noCompletedPo", "(篩選範圍內無已完成之 PO 行)"), | |||
| categoryCurrencyTotal: tx(t, "excel.grn.categoryCurrencyTotal", "貨幣小計"), | |||
| categoryPo: tx(t, "excel.grn.categoryPo", "訂單"), | |||
| }; | |||
| } | |||
| function toExcelRow( | |||
| r: GrnReportRow, | |||
| includeFinancialColumns: boolean | |||
| includeFinancialColumns: boolean, | |||
| t?: TFunction, | |||
| ): Record<string, string | number | undefined> { | |||
| const L = grnLabels(t); | |||
| const base: Record<string, string | number | undefined> = { | |||
| "PO No. / 訂單編號": r.poCode ?? "", | |||
| "Delivery Note No. / 送貨單編號": r.deliveryNoteNo ?? "", | |||
| "Receipt Date / 收貨日期": r.receiptDate ?? "", | |||
| "Item Code / 物料編號": r.itemCode ?? "", | |||
| "Item Name / 物料名稱": r.itemName ?? "", | |||
| "Qty / 數量": formatQty( | |||
| r.acceptedQty ?? r.receivedQty ?? undefined | |||
| ), | |||
| "Demand Qty / 訂單數量": formatQty(r.demandQty), | |||
| "UOM / 單位": r.uom ?? r.purchaseUomDesc ?? r.stockUomDesc ?? "", | |||
| "Supplier Lot No. 供應商批次": r.productLotNo ?? "", | |||
| "Expiry Date / 到期日": r.expiryDate ?? "", | |||
| "Supplier Code / 供應商編號": r.supplierCode ?? "", | |||
| "Supplier / 供應商": r.supplier ?? "", | |||
| "入倉狀態": r.status ?? "", | |||
| [L.poNo]: r.poCode ?? "", | |||
| [L.deliveryNoteNo]: r.deliveryNoteNo ?? "", | |||
| [L.receiptDate]: r.receiptDate ?? "", | |||
| [L.itemCode]: r.itemCode ?? "", | |||
| [L.itemName]: r.itemName ?? "", | |||
| [L.qty]: formatQty(r.acceptedQty ?? r.receivedQty ?? undefined), | |||
| [L.demandQty]: formatQty(r.demandQty), | |||
| [L.uom]: r.uom ?? r.purchaseUomDesc ?? r.stockUomDesc ?? "", | |||
| [L.supplierLotNo]: r.productLotNo ?? "", | |||
| [L.expiryDate]: r.expiryDate ?? "", | |||
| [L.supplierCode]: r.supplierCode ?? "", | |||
| [L.supplier]: r.supplier ?? "", | |||
| [L.status]: r.status ?? "", | |||
| }; | |||
| if (includeFinancialColumns) { | |||
| base["Unit Price / 單價"] = moneyCellValue(r.unitPrice); | |||
| base["Currency / 貨幣"] = r.currencyCode ?? ""; | |||
| base["Amount / 金額"] = moneyCellValue(r.lineAmount); | |||
| base[L.unitPrice] = moneyCellValue(r.unitPrice); | |||
| base[L.currency] = r.currencyCode ?? ""; | |||
| base[L.amount] = moneyCellValue(r.lineAmount); | |||
| } | |||
| base["GRN Code / M18 入倉單號"] = r.grnCode ?? ""; | |||
| base["GRN Id / M18 記錄編號"] = r.grnId ?? ""; | |||
| base["PO建立者(M18) / PO creator (M18)"] = r.poM18CreatorDisplay ?? ""; | |||
| base[L.grnCode] = r.grnCode ?? ""; | |||
| base[L.grnId] = r.grnId ?? ""; | |||
| base[L.poCreator] = r.poM18CreatorDisplay ?? ""; | |||
| return base; | |||
| } | |||
| const GRN_SHEET_DETAIL = "PO入倉記錄"; | |||
| const GRN_SHEET_LISTED_PO = "已上架PO金額"; | |||
| /** Rows for sheet "已上架PO金額" (ADMIN-only; do not add this sheet for other users). */ | |||
| function buildListedPoAmountSheetRows( | |||
| listed: ListedPoAmounts | undefined | |||
| listed: ListedPoAmounts | undefined, | |||
| t?: TFunction, | |||
| ): Record<string, string | number | undefined>[] { | |||
| const L = grnLabels(t); | |||
| if ( | |||
| !listed || | |||
| (listed.currencyTotals.length === 0 && | |||
| listed.byPurchaseOrder.length === 0) | |||
| ) { | |||
| return [ | |||
| { | |||
| "Note / 備註": | |||
| "(篩選範圍內無已完成之 PO 行) / No completed PO lines in the selected range", | |||
| }, | |||
| ]; | |||
| return [{ [L.note]: L.noCompletedPo }]; | |||
| } | |||
| const out: Record<string, string | number | undefined>[] = []; | |||
| for (const c of listed.currencyTotals) { | |||
| out.push({ | |||
| "Category / 類別": "貨幣小計 / Currency total", | |||
| "Receipt Date / 收貨日期": c.receiptDate ?? "", | |||
| "PO No. / 訂單編號": "", | |||
| "Currency / 貨幣": c.currencyCode ?? "", | |||
| "Total Amount / 金額": moneyCellValue(c.totalAmount), | |||
| "GRN Code(s) / M18 入倉單號": "", | |||
| [L.category]: L.categoryCurrencyTotal, | |||
| [L.receiptDate]: c.receiptDate ?? "", | |||
| [L.poNo]: "", | |||
| [L.currency]: c.currencyCode ?? "", | |||
| [L.totalAmount]: moneyCellValue(c.totalAmount), | |||
| [L.grnCodes]: "", | |||
| }); | |||
| } | |||
| for (const p of listed.byPurchaseOrder) { | |||
| out.push({ | |||
| "Category / 類別": "訂單 / PO", | |||
| "Receipt Date / 收貨日期": p.receiptDate ?? "", | |||
| "PO No. / 訂單編號": p.poCode ?? "", | |||
| "Currency / 貨幣": p.currencyCode ?? "", | |||
| "Total Amount / 金額": moneyCellValue(p.totalAmount), | |||
| "GRN Code(s) / M18 入倉單號": p.grnCodes ?? "", | |||
| [L.category]: L.categoryPo, | |||
| [L.receiptDate]: p.receiptDate ?? "", | |||
| [L.poNo]: p.poCode ?? "", | |||
| [L.currency]: p.currencyCode ?? "", | |||
| [L.totalAmount]: moneyCellValue(p.totalAmount), | |||
| [L.grnCodes]: p.grnCodes ?? "", | |||
| }); | |||
| } | |||
| return out; | |||
| @@ -206,10 +233,11 @@ export async function generateGrnReportExcel( | |||
| criteria: Record<string, string>, | |||
| reportTitle: string = "PO 入倉記錄", | |||
| /** Only users with ADMIN authority should pass true (must match backend). */ | |||
| includeFinancialColumns: boolean = false | |||
| includeFinancialColumns: boolean = false, | |||
| t?: TFunction, | |||
| ): Promise<void> { | |||
| const { rows, listedPoAmounts } = await fetchGrnReportData(criteria); | |||
| const excelRows = rows.map((r) => toExcelRow(r, includeFinancialColumns)); | |||
| const excelRows = rows.map((r) => toExcelRow(r, includeFinancialColumns, t)); | |||
| const start = criteria.receiptDateStart; | |||
| const end = criteria.receiptDateEnd; | |||
| let datePart: string; | |||
| @@ -222,17 +250,18 @@ export async function generateGrnReportExcel( | |||
| } | |||
| const safeDatePart = datePart.replace(/[^\d\-_/]/g, ""); | |||
| const filename = `${reportTitle}_${safeDatePart}`; | |||
| const L = grnLabels(t); | |||
| if (includeFinancialColumns) { | |||
| const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts); | |||
| const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts, t); | |||
| exportMultiSheetToXlsx( | |||
| [ | |||
| { name: GRN_SHEET_DETAIL, rows: excelRows as Record<string, unknown>[] }, | |||
| { name: GRN_SHEET_LISTED_PO, rows: sheet2 as Record<string, unknown>[] }, | |||
| { name: L.sheetDetail, rows: excelRows as Record<string, unknown>[] }, | |||
| { name: L.sheetListedPo, rows: sheet2 as Record<string, unknown>[] }, | |||
| ], | |||
| filename | |||
| ); | |||
| } else { | |||
| exportChartToXlsx(excelRows as Record<string, unknown>[], filename, GRN_SHEET_DETAIL); | |||
| exportChartToXlsx(excelRows as Record<string, unknown>[], filename, L.sheetDetail); | |||
| } | |||
| } | |||
| @@ -20,11 +20,12 @@ import { | |||
| FormControlLabel, | |||
| } from '@mui/material'; | |||
| import DownloadIcon from '@mui/icons-material/Download'; | |||
| import { REPORTS, ReportDefinition } from '@/config/reportConfig'; | |||
| import { REPORTS } from '@/config/reportConfig'; | |||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | |||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | |||
| import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | |||
| import ReportSelectionDashboard from './ReportSelectionDashboard'; | |||
| import { useReportLabels } from './reportI18n'; | |||
| import { | |||
| fetchSemiFGItemCodes, | |||
| fetchSemiFGItemCodesWithCategory | |||
| @@ -47,6 +48,7 @@ interface ItemCodeWithName { | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */ | |||
| export default function ReportPage() { | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const { t, reportTitle, fieldLabel, optionLabel } = useReportLabels(); | |||
| const includeGrnFinancialColumns = | |||
| session?.abilities?.includes(AUTH.ADMIN) ?? false; | |||
| @@ -196,7 +198,9 @@ export default function ReportPage() { | |||
| if (currentReport.id === 'rep-012') { | |||
| if (rep012RoundIds.length === 0) { | |||
| alert('缺少必填條件:\n- 盤點輪次'); | |||
| alert(t('missingRequired', { | |||
| fields: fieldLabel('rep-012', { name: 'stockTakeRoundId', label: '盤點輪次' }), | |||
| })); | |||
| return false; | |||
| } | |||
| return true; | |||
| @@ -208,10 +212,10 @@ export default function ReportPage() { | |||
| if (!field.required) return false; | |||
| return !criteria[field.name]; | |||
| }) | |||
| .map(field => field.label); | |||
| .map((field) => fieldLabel(currentReport.id, field)); | |||
| if (missingFields.length > 0) { | |||
| alert(`缺少必填條件:\n- ${missingFields.join('\n- ')}`); | |||
| alert(t('missingRequired', { fields: missingFields.join('\n- ') })); | |||
| return false; | |||
| } | |||
| @@ -224,10 +228,10 @@ export default function ReportPage() { | |||
| const v = (criteria[field.name] || '').trim(); | |||
| return v && v < todayStr; | |||
| }) | |||
| .map((field) => field.label); | |||
| .map((field) => fieldLabel(currentReport.id, field)); | |||
| if (beforeToday.length > 0) { | |||
| alert(`日期不可早於今天:\n- ${beforeToday.join('\n- ')}`); | |||
| alert(t('dateNotBeforeToday', { fields: beforeToday.join('\n- ') })); | |||
| return false; | |||
| } | |||
| @@ -304,13 +308,14 @@ export default function ReportPage() { | |||
| if (currentReport.id === 'rep-014') { | |||
| await generateGrnReportExcel( | |||
| criteria, | |||
| currentReport.title, | |||
| includeGrnFinancialColumns | |||
| reportTitle(currentReport), | |||
| includeGrnFinancialColumns, | |||
| t, | |||
| ); | |||
| } else if (currentReport.id === 'rep-015') { | |||
| await generateBomShopSyncReportExcel(criteria, currentReport.title); | |||
| await generateBomShopSyncReportExcel(criteria, reportTitle(currentReport), t); | |||
| } else if (currentReport.id === 'rep-017') { | |||
| await generateShopOrderReplenishmentReportExcel(criteria, currentReport.title); | |||
| await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t); | |||
| } else { | |||
| // Backend returns actual .xlsx bytes for this Excel endpoint. | |||
| let queryParams = | |||
| @@ -349,7 +354,7 @@ export default function ReportPage() { | |||
| link.href = downloadUrl; | |||
| const contentDisposition = response.headers.get('Content-Disposition'); | |||
| let fileName = `${currentReport.title}.xlsx`; | |||
| let fileName = `${reportTitle(currentReport)}.xlsx`; | |||
| if (contentDisposition?.includes('filename=')) { | |||
| fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); | |||
| } | |||
| @@ -370,7 +375,7 @@ export default function ReportPage() { | |||
| setShowConfirmDialog(false); | |||
| } catch (error) { | |||
| console.error("Failed to generate Excel report:", error); | |||
| alert("An error occurred while generating the report. Please try again."); | |||
| alert(t('generateError')); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| @@ -407,7 +412,7 @@ export default function ReportPage() { | |||
| link.href = downloadUrl; | |||
| const contentDisposition = response.headers.get('Content-Disposition'); | |||
| let fileName = `${currentReport.title}.pdf`; | |||
| let fileName = `${reportTitle(currentReport)}.pdf`; | |||
| if (contentDisposition?.includes('filename=')) { | |||
| fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); | |||
| } | |||
| @@ -427,7 +432,7 @@ export default function ReportPage() { | |||
| setShowConfirmDialog(false); | |||
| } catch (error) { | |||
| console.error("Failed to generate report:", error); | |||
| alert("An error occurred while generating the report. Please try again."); | |||
| alert(t('generateError')); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| @@ -436,7 +441,7 @@ export default function ReportPage() { | |||
| return ( | |||
| <Box sx={{ p: 4, maxWidth: 1280, margin: '0 auto' }}> | |||
| <Typography variant="h4" gutterBottom fontWeight="bold"> | |||
| 報告管理 | |||
| {t('title')} | |||
| </Typography> | |||
| <ReportSelectionDashboard | |||
| @@ -448,15 +453,19 @@ export default function ReportPage() { | |||
| <Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}> | |||
| <CardContent> | |||
| <Typography variant="h6" color="primary" gutterBottom> | |||
| 搜索條件: {currentReport.title} | |||
| {t('searchCriteriaWithTitle', { title: reportTitle(currentReport) })} | |||
| </Typography> | |||
| <Divider sx={{ mb: 3 }} /> | |||
| <Grid container spacing={3}> | |||
| {currentReport.fields.map((field) => { | |||
| const translatedLabel = fieldLabel(currentReport.id, field); | |||
| const options = field.dynamicOptions | |||
| ? (dynamicOptions[field.name] || []) | |||
| : (field.options || []); | |||
| : (field.options || []).map((opt) => ({ | |||
| ...opt, | |||
| label: optionLabel(currentReport.id, field.name, opt), | |||
| })); | |||
| const currentValue = criteria[field.name] || ''; | |||
| const valueForSelect = field.multiple | |||
| ? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : []) | |||
| @@ -491,7 +500,7 @@ export default function ReportPage() { | |||
| } | |||
| /> | |||
| } | |||
| label={field.label} | |||
| label={translatedLabel} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| @@ -547,8 +556,8 @@ export default function ReportPage() { | |||
| <TextField | |||
| {...params} | |||
| fullWidth | |||
| label={field.label} | |||
| placeholder={field.placeholder || "選擇或輸入物料編號"} | |||
| label={translatedLabel} | |||
| placeholder={field.placeholder || t('selectOrEnterItemCode')} | |||
| sx={currentReport.id === 'rep-005' ? { | |||
| '& .MuiOutlinedInput-root': { | |||
| minHeight: '64px', | |||
| @@ -590,7 +599,7 @@ export default function ReportPage() { | |||
| <Grid item {...gridSize} key={field.name}> | |||
| <TextField | |||
| fullWidth | |||
| label={field.label} | |||
| label={translatedLabel} | |||
| type={field.type} | |||
| placeholder={field.placeholder} | |||
| disabled={disabledByCheckedCheckbox || disabledRep012Status} | |||
| @@ -683,10 +692,10 @@ export default function ReportPage() { | |||
| {currentReport.id === 'rep-005' ? ( | |||
| <SemiFGProductionAnalysisReport | |||
| criteria={criteria} | |||
| requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => f.label)} | |||
| requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => fieldLabel(currentReport.id, f))} | |||
| loading={loading} | |||
| setLoading={setLoading} | |||
| reportTitle={currentReport.title} | |||
| reportTitle={reportTitle(currentReport)} | |||
| onExportSuccess={(format) => { | |||
| logFeatureUsage( | |||
| FEATURE_USAGE.REPORT_MANAGEMENT, | |||
| @@ -705,7 +714,7 @@ export default function ReportPage() { | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? "生成 PDF..." : "下載報告 (PDF)"} | |||
| {loading ? t('generatingPdf') : t('downloadPdf')} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -715,7 +724,7 @@ export default function ReportPage() { | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||
| </Button> | |||
| </> | |||
| ) : currentReport.id === 'rep-006' || currentReport.id === 'rep-010' ? ( | |||
| @@ -728,7 +737,7 @@ export default function ReportPage() { | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? "生成 PDF..." : "下載報告 (PDF)"} | |||
| {loading ? t('generatingPdf') : t('downloadPdf')} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -738,7 +747,7 @@ export default function ReportPage() { | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||
| </Button> | |||
| </> | |||
| ) : currentReport.responseType === 'excel' ? ( | |||
| @@ -750,7 +759,7 @@ export default function ReportPage() { | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||
| </Button> | |||
| ) : ( | |||
| <Button | |||
| @@ -761,7 +770,7 @@ export default function ReportPage() { | |||
| disabled={loading} | |||
| sx={{ px: 4 }} | |||
| > | |||
| {loading ? "生成報告..." : "下載報告 (PDF)"} | |||
| {loading ? t('generatingReport') : t('downloadPdf')} | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| @@ -0,0 +1,47 @@ | |||
| "use client"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import type { TFunction } from "i18next"; | |||
| import type { ReportDefinition, ReportField } from "@/config/reportConfig"; | |||
| export function useReportLabels() { | |||
| const { t } = useTranslation("report"); | |||
| const reportTitle = (report: Pick<ReportDefinition, "id" | "title">) => | |||
| t(`reports.${report.id}.title`, { defaultValue: report.title }); | |||
| const fieldLabel = ( | |||
| reportId: string, | |||
| field: Pick<ReportField, "name" | "label">, | |||
| ) => | |||
| t(`reports.${reportId}.fields.${field.name}`, { | |||
| defaultValue: field.label, | |||
| }); | |||
| const optionLabel = ( | |||
| reportId: string, | |||
| fieldName: string, | |||
| opt: { label: string; value: string }, | |||
| ) => | |||
| t( | |||
| [ | |||
| `reports.${reportId}.options.${fieldName}.${opt.value}`, | |||
| `options.${opt.value}`, | |||
| ], | |||
| { defaultValue: opt.label }, | |||
| ); | |||
| const categoryTitle = (id: string, fallback: string) => | |||
| t(`categories.${id}`, { defaultValue: fallback }); | |||
| return { t, reportTitle, fieldLabel, optionLabel, categoryTitle }; | |||
| } | |||
| export function reportExcelT( | |||
| t: TFunction | undefined, | |||
| key: string, | |||
| fallback: string, | |||
| ): string { | |||
| if (!t) return fallback; | |||
| return String(t(key, { defaultValue: fallback })); | |||
| } | |||
| @@ -3,6 +3,8 @@ | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| import { exportChartToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx"; | |||
| import { reportExcelT as tx } from "./reportI18n"; | |||
| import type { TFunction } from "i18next"; | |||
| export interface ShopOrderReplenishmentReportRow { | |||
| shopNo?: string; | |||
| @@ -27,38 +29,75 @@ export interface ShopOrderReplenishmentReportResponse { | |||
| rows: ShopOrderReplenishmentReportRow[]; | |||
| } | |||
| const SHEET_NAME = "店鋪訂單補貨記錄"; | |||
| function shopReplenishmentLabels(t?: TFunction) { | |||
| return { | |||
| sheetName: tx(t, "excel.shopReplenishment.sheetName", "店鋪訂單補貨記錄"), | |||
| noData: tx(t, "excel.noData", "(篩選範圍內無資料)"), | |||
| shopCode: tx(t, "excel.shopReplenishment.shopCode", "店鋪編號"), | |||
| shopName: tx(t, "excel.shopReplenishment.shopName", "店鋪名稱"), | |||
| shopOrderDate: tx(t, "excel.shopReplenishment.shopOrderDate", "店鋪訂單日期"), | |||
| shopOrderNo: tx(t, "excel.shopReplenishment.shopOrderNo", "店鋪訂單編號"), | |||
| itemCode: tx(t, "excel.shopReplenishment.itemCode", "貨品編號"), | |||
| itemName: tx(t, "excel.shopReplenishment.itemName", "貨品名稱"), | |||
| firstOrderQty: tx(t, "excel.shopReplenishment.firstOrderQty", "原訂單數量"), | |||
| firstOrderActualPickQty: tx( | |||
| t, | |||
| "excel.shopReplenishment.firstOrderActualPickQty", | |||
| "原單實際提料數量", | |||
| ), | |||
| firstOrderPicker: tx(t, "excel.shopReplenishment.firstOrderPicker", "原單提料人"), | |||
| reorderQty: tx(t, "excel.shopReplenishment.reorderQty", "補貨數量"), | |||
| reorderDate: tx(t, "excel.shopReplenishment.reorderDate", "補貨日期"), | |||
| reason: tx(t, "excel.shopReplenishment.reason", "補貨原因"), | |||
| actualDeliveredQty: tx(t, "excel.shopReplenishment.actualDeliveredQty", "實際補貨數量"), | |||
| actualDeliveredHandler: tx( | |||
| t, | |||
| "excel.shopReplenishment.actualDeliveredHandler", | |||
| "實際補貨提料人", | |||
| ), | |||
| deliveredDate: tx(t, "excel.shopReplenishment.deliveredDate", "送貨日期"), | |||
| reasonQuality: tx(t, "excel.shopReplenishment.reasonQuality", "質素問題"), | |||
| reasonOutOfStock: tx(t, "excel.shopReplenishment.reasonOutOfStock", "缺貨"), | |||
| reasonOther: tx(t, "excel.shopReplenishment.reasonOther", "其他"), | |||
| }; | |||
| } | |||
| const NO_DATA_NOTE = "(篩選範圍內無資料)"; | |||
| type ShopReplenishmentLabels = ReturnType<typeof shopReplenishmentLabels>; | |||
| function emptySheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> { | |||
| function emptySheetRow( | |||
| L: ShopReplenishmentLabels, | |||
| note?: string, | |||
| ): Record<string, unknown> { | |||
| return { | |||
| "店鋪編號": note, | |||
| "店鋪名稱": "", | |||
| "店鋪訂單日期": "", | |||
| "店鋪訂單編號": "", | |||
| "貨品編號": "", | |||
| "貨品名稱": "", | |||
| "原訂單數量": "", | |||
| "原單實際提料數量": "", | |||
| "原單提料人": "", | |||
| "補貨數量": "", | |||
| "補貨日期": "", | |||
| "補貨原因": "", | |||
| "實際補貨數量": "", | |||
| "實際補貨提料人": "", | |||
| "送貨日期": "", | |||
| [L.shopCode]: note ?? L.noData, | |||
| [L.shopName]: "", | |||
| [L.shopOrderDate]: "", | |||
| [L.shopOrderNo]: "", | |||
| [L.itemCode]: "", | |||
| [L.itemName]: "", | |||
| [L.firstOrderQty]: "", | |||
| [L.firstOrderActualPickQty]: "", | |||
| [L.firstOrderPicker]: "", | |||
| [L.reorderQty]: "", | |||
| [L.reorderDate]: "", | |||
| [L.reason]: "", | |||
| [L.actualDeliveredQty]: "", | |||
| [L.actualDeliveredHandler]: "", | |||
| [L.deliveredDate]: "", | |||
| }; | |||
| } | |||
| function formatReason(reason: string | undefined): string { | |||
| function formatReason( | |||
| reason: string | undefined, | |||
| L: ShopReplenishmentLabels, | |||
| ): string { | |||
| switch ((reason ?? "").trim()) { | |||
| case "quality_issue": | |||
| return "質素問題"; | |||
| return L.reasonQuality; | |||
| case "out_of_stock": | |||
| return "缺貨"; | |||
| return L.reasonOutOfStock; | |||
| case "other": | |||
| return "其他"; | |||
| return L.reasonOther; | |||
| default: | |||
| return reason ?? ""; | |||
| } | |||
| @@ -91,25 +130,27 @@ function formatQty(value: unknown): string | number { | |||
| return n; | |||
| } | |||
| function toExcelRow(r: ShopOrderReplenishmentReportRow): Record<string, unknown> { | |||
| const base = emptySheetRow(""); | |||
| function toExcelRow( | |||
| r: ShopOrderReplenishmentReportRow, | |||
| L: ShopReplenishmentLabels, | |||
| ): Record<string, unknown> { | |||
| return { | |||
| ...base, | |||
| "店鋪編號": r.shopNo ?? "", | |||
| "店鋪名稱": r.shopName ?? "", | |||
| "店鋪訂單日期": formatDateCell(r.shopOrderDate), | |||
| "店鋪訂單編號": r.shopOrderNo ?? "", | |||
| "貨品編號": r.itemNo ?? "", | |||
| "貨品名稱": r.itemName ?? "", | |||
| "原訂單數量": formatQty(r.firstOrderQty), | |||
| "原單實際提料數量": formatQty(r.firstOrderActualPickQty), | |||
| "原單提料人": r.firstOrderPickerHandler ?? "", | |||
| "補貨數量": formatQty(r.reorderQty), | |||
| "補貨日期": formatDateCell(r.reorderDate), | |||
| "補貨原因": formatReason(r.reason), | |||
| "實際補貨數量": formatQty(r.actualDeliveredQty), | |||
| "實際補貨提料人": r.actualDeliveredHandler ?? "", | |||
| "送貨日期": formatDateCell(r.deliveredDate), | |||
| ...emptySheetRow(L, ""), | |||
| [L.shopCode]: r.shopNo ?? "", | |||
| [L.shopName]: r.shopName ?? "", | |||
| [L.shopOrderDate]: formatDateCell(r.shopOrderDate), | |||
| [L.shopOrderNo]: r.shopOrderNo ?? "", | |||
| [L.itemCode]: r.itemNo ?? "", | |||
| [L.itemName]: r.itemName ?? "", | |||
| [L.firstOrderQty]: formatQty(r.firstOrderQty), | |||
| [L.firstOrderActualPickQty]: formatQty(r.firstOrderActualPickQty), | |||
| [L.firstOrderPicker]: r.firstOrderPickerHandler ?? "", | |||
| [L.reorderQty]: formatQty(r.reorderQty), | |||
| [L.reorderDate]: formatDateCell(r.reorderDate), | |||
| [L.reason]: formatReason(r.reason, L), | |||
| [L.actualDeliveredQty]: formatQty(r.actualDeliveredQty), | |||
| [L.actualDeliveredHandler]: r.actualDeliveredHandler ?? "", | |||
| [L.deliveredDate]: formatDateCell(r.deliveredDate), | |||
| }; | |||
| } | |||
| @@ -143,10 +184,12 @@ export async function fetchShopOrderReplenishmentReportData( | |||
| export async function generateShopOrderReplenishmentReportExcel( | |||
| criteria: Record<string, string>, | |||
| reportTitle: string = "店鋪訂單補貨記錄", | |||
| t?: TFunction, | |||
| ): Promise<void> { | |||
| const L = shopReplenishmentLabels(t); | |||
| const rows = await fetchShopOrderReplenishmentReportData(criteria); | |||
| const excelRows = | |||
| rows.length > 0 ? rows.map(toExcelRow) : [emptySheetRow()]; | |||
| rows.length > 0 ? rows.map((r) => toExcelRow(r, L)) : [emptySheetRow(L)]; | |||
| const dateCandidates = [ | |||
| criteria.reorderDateStart, | |||
| @@ -163,5 +206,5 @@ export async function generateShopOrderReplenishmentReportExcel( | |||
| const safeDatePart = datePart.replace(/[^\d\-_/]/g, ""); | |||
| const filename = `${reportTitle}_${safeDatePart}`; | |||
| exportChartToXlsx(excelRows, filename, SHEET_NAME); | |||
| exportChartToXlsx(excelRows, filename, L.sheetName); | |||
| } | |||
| @@ -1,13 +1,291 @@ | |||
| { | |||
| "Report": "Report", | |||
| "title": "Report Management", | |||
| "selectReport": "Select Report", | |||
| "reportList": "Report List", | |||
| "selectReportHelper": "Select a report", | |||
| "searchCriteria": "Search Criteria", | |||
| "searchCriteriaWithTitle": "Search Criteria: {{title}}", | |||
| "downloadPdf": "Download Report (PDF)", | |||
| "downloadExcel": "Download Report (Excel)", | |||
| "generatingPdf": "Generating PDF...", | |||
| "generatingExcel": "Generating Excel...", | |||
| "generatingReport": "Generating report..." | |||
| "generatingReport": "Generating report...", | |||
| "generateError": "An error occurred while generating the report. Please try again.", | |||
| "missingRequired": "Missing required fields:\n- {{fields}}", | |||
| "dateNotBeforeToday": "Date cannot be before today:\n- {{fields}}", | |||
| "selectOrEnterItemCode": "Select or enter item code", | |||
| "cancel": "Cancel", | |||
| "confirmDownloadPdf": "Confirm download PDF", | |||
| "confirmDownloadExcel": "Confirm download Excel", | |||
| "semiFgConfirmTitle": "Selected item codes — FG / Semi-FG Production Analysis Report", | |||
| "semiFgConfirmHint": "Please confirm the selected item codes and their categories:", | |||
| "semiFgColItem": "Item code and name", | |||
| "semiFgColCategory": "Category", | |||
| "qcScopeHelpAll": "Export all QC inspection items (including temperature / humidity).", | |||
| "qcScopeHelpMeasurable": "Export temperature / humidity QC items only (same as the previous production default).", | |||
| "categories": { | |||
| "inventory": "Inventory Management", | |||
| "inbound-outbound": "Inbound / Outbound", | |||
| "production": "Production & Trends" | |||
| }, | |||
| "options": { | |||
| "All": "All", | |||
| "all": "All", | |||
| "pending": "Pending", | |||
| "completed": "Approved", | |||
| "success": "Success", | |||
| "failed": "Failed", | |||
| "measurable": "Temperature & humidity only" | |||
| }, | |||
| "reports": { | |||
| "rep-004": { | |||
| "title": "Stock-in Traceability Report", | |||
| "fields": { | |||
| "lastInDateStart": "Last In Date Start", | |||
| "lastInDateEnd": "Last In Date End", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-008": { | |||
| "title": "Finished Goods Delivery Report", | |||
| "fields": { | |||
| "lastOutDateStart": "Last Out Date Start", | |||
| "lastOutDateEnd": "Last Out Date End", | |||
| "year": "Year", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-012": { | |||
| "title": "Stock Take Report", | |||
| "fields": { | |||
| "stockTakeRoundId": "Stock Take Round (multi-select)", | |||
| "itemCode": "Item Code", | |||
| "store_id": "Warehouse Floor", | |||
| "status": "Status", | |||
| "type": "Type" | |||
| }, | |||
| "options": { | |||
| "store_id": { | |||
| "All": "All" | |||
| }, | |||
| "status": { | |||
| "All": "All", | |||
| "pending": "Pending", | |||
| "completed": "Approved" | |||
| }, | |||
| "type": { | |||
| "All": "All", | |||
| "PP": "PP", | |||
| "PF": "PF", | |||
| "TOA": "TOA", | |||
| "工廠生產": "Factory production", | |||
| "倉存調整": "Inventory adjustment", | |||
| "期初存貨": "Opening inventory", | |||
| "採購入倉": "Purchase inbound", | |||
| "其他入倉": "Other inbound" | |||
| } | |||
| } | |||
| }, | |||
| "rep-011": { | |||
| "title": "Stock Ledger Report", | |||
| "fields": { | |||
| "lastInDateStart": "Stock Date Start", | |||
| "lastInDateEnd": "Stock Date End", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-007": { | |||
| "title": "Stock Balance Report", | |||
| "fields": { | |||
| "stockDate": "Stock Date", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-014": { | |||
| "title": "PO Goods Receipt Report", | |||
| "fields": { | |||
| "receiptDateStart": "Receipt Date Start", | |||
| "receiptDateEnd": "Receipt Date End", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-009": { | |||
| "title": "Finished Goods Stock-out Traceability Report", | |||
| "fields": { | |||
| "lastOutDateStart": "Last Out Date Start", | |||
| "lastOutDateEnd": "Last Out Date End", | |||
| "itemCode": "Item Code", | |||
| "handler": "Handler" | |||
| } | |||
| }, | |||
| "rep-010": { | |||
| "title": "Inventory QC Report", | |||
| "fields": { | |||
| "lastInDateStart": "QC Date Start", | |||
| "lastInDateEnd": "QC Date End", | |||
| "qcType": "QC Type", | |||
| "itemCode": "Item Code", | |||
| "qcItemScope": "QC Item Scope" | |||
| }, | |||
| "options": { | |||
| "qcType": { | |||
| "all": "All", | |||
| "IQC": "IQC (Purchase)", | |||
| "EPQC": "EPQC (Job Order)" | |||
| }, | |||
| "qcItemScope": { | |||
| "all": "All QC items", | |||
| "measurable": "Temperature & humidity only" | |||
| } | |||
| } | |||
| }, | |||
| "rep-013": { | |||
| "title": "Material Stock-out Traceability Report", | |||
| "fields": { | |||
| "lastOutDateStart": "Last Out Date Start", | |||
| "lastOutDateEnd": "Last Out Date End", | |||
| "itemCode": "Item Code", | |||
| "handler": "Handler" | |||
| } | |||
| }, | |||
| "rep-006": { | |||
| "title": "Stock Item Consumption Trend Report", | |||
| "fields": { | |||
| "lastOutDateStart": "Consumption Date Start", | |||
| "lastOutDateEnd": "Consumption Date End", | |||
| "year": "Year", | |||
| "stockCategory": "Category", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-005": { | |||
| "title": "FG / Semi-FG Production Analysis Report", | |||
| "fields": { | |||
| "lastOutDateStart": "Production Complete Date Start", | |||
| "lastOutDateEnd": "Production Complete Date End", | |||
| "year": "Year", | |||
| "stockCategory": "Category", | |||
| "itemCode": "Item Code" | |||
| } | |||
| }, | |||
| "rep-015": { | |||
| "title": "M18 BOM Shop Sync History", | |||
| "fields": { | |||
| "syncDateStart": "Sync Date Start", | |||
| "syncDateEnd": "Sync Date End", | |||
| "finishedItemCode": "Finished Item Code", | |||
| "syncStatus": "Sync Status" | |||
| }, | |||
| "options": { | |||
| "syncStatus": { | |||
| "all": "All", | |||
| "success": "Success", | |||
| "failed": "Failed" | |||
| } | |||
| } | |||
| }, | |||
| "rep-016": { | |||
| "title": "FG Delivery Pick Compliance Report", | |||
| "fields": { | |||
| "dateStart": "Date", | |||
| "handler": "Handler", | |||
| "ticketNo": "Ticket No.", | |||
| "itemCode": "Item Code", | |||
| "storeId": "Floor" | |||
| } | |||
| }, | |||
| "rep-017": { | |||
| "title": "Shop Order Replenishment Record", | |||
| "fields": { | |||
| "shopOrderDateStart": "Shop Order Date Start", | |||
| "shopOrderDateEnd": "Shop Order Date End", | |||
| "shopCode": "Shop Code" | |||
| } | |||
| }, | |||
| "rep-018": { | |||
| "title": "Delivery Order vs Inventory UOM Mismatch Report", | |||
| "fields": { | |||
| "deliveryDate": "Estimated Arrival Date", | |||
| "storeId": "Delivery Order Floor" | |||
| }, | |||
| "options": { | |||
| "storeId": { | |||
| "All": "All" | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| "excel": { | |||
| "noData": "(No records in the selected range)", | |||
| "grn": { | |||
| "sheetDetail": "PO Goods Receipt", | |||
| "sheetListedPo": "Listed PO Amounts", | |||
| "noCompletedPo": "No completed PO lines in the selected range", | |||
| "categoryCurrencyTotal": "Currency total", | |||
| "categoryPo": "PO", | |||
| "poNo": "PO No.", | |||
| "deliveryNoteNo": "Delivery Note No.", | |||
| "receiptDate": "Receipt Date", | |||
| "itemCode": "Item Code", | |||
| "itemName": "Item Name", | |||
| "qty": "Qty", | |||
| "demandQty": "Demand Qty", | |||
| "uom": "UOM", | |||
| "supplierLotNo": "Supplier Lot No.", | |||
| "expiryDate": "Expiry Date", | |||
| "supplierCode": "Supplier Code", | |||
| "supplier": "Supplier", | |||
| "status": "Stock-in Status", | |||
| "unitPrice": "Unit Price", | |||
| "currency": "Currency", | |||
| "amount": "Amount", | |||
| "grnCode": "GRN Code / M18 Receipt No.", | |||
| "grnId": "GRN Id / M18 Record Id", | |||
| "poCreator": "PO creator (M18)", | |||
| "note": "Note", | |||
| "category": "Category", | |||
| "totalAmount": "Total Amount", | |||
| "grnCodes": "GRN Code(s) / M18 Receipt No." | |||
| }, | |||
| "bomSync": { | |||
| "sheetSync": "BOM Sync Log", | |||
| "sheetMaterials": "BOM Material Lines", | |||
| "syncTime": "Sync Time", | |||
| "finishedItemCode": "Finished Item Code", | |||
| "finishedItemName": "Finished Item Name", | |||
| "bomRoutingCode": "BOM Routing Code", | |||
| "version": "Version", | |||
| "status": "Status", | |||
| "failureReason": "Failure Reason", | |||
| "message": "Message", | |||
| "lineNo": "Line No.", | |||
| "materialName": "Material Name", | |||
| "uom": "UOM", | |||
| "qty": "Qty", | |||
| "statusSuccess": "Success", | |||
| "statusSkipped": "Skipped (unchanged)", | |||
| "statusFailed": "Failed" | |||
| }, | |||
| "shopReplenishment": { | |||
| "sheetName": "Shop Order Replenishment", | |||
| "shopCode": "Shop Code", | |||
| "shopName": "Shop Name", | |||
| "shopOrderDate": "Shop Order Date", | |||
| "shopOrderNo": "Shop Order No.", | |||
| "itemCode": "Item Code", | |||
| "itemName": "Item Name", | |||
| "firstOrderQty": "Original Order Qty", | |||
| "firstOrderActualPickQty": "Original Actual Pick Qty", | |||
| "firstOrderPicker": "Original Picker", | |||
| "reorderQty": "Replenish Qty", | |||
| "reorderDate": "Replenish Date", | |||
| "reason": "Replenish Reason", | |||
| "actualDeliveredQty": "Actual Replenish Qty", | |||
| "actualDeliveredHandler": "Actual Replenish Handler", | |||
| "deliveredDate": "Delivery Date", | |||
| "reasonQuality": "Quality issue", | |||
| "reasonOutOfStock": "Out of stock", | |||
| "reasonOther": "Other" | |||
| } | |||
| } | |||
| } | |||
| @@ -1,13 +1,291 @@ | |||
| { | |||
| "Report": "報告", | |||
| "title": "報告管理", | |||
| "selectReport": "選擇報告", | |||
| "reportList": "報告列表", | |||
| "selectReportHelper": "選擇報告", | |||
| "searchCriteria": "搜索條件", | |||
| "searchCriteriaWithTitle": "搜索條件: {{title}}", | |||
| "downloadPdf": "下載報告 (PDF)", | |||
| "downloadExcel": "下載報告 (Excel)", | |||
| "generatingPdf": "生成 PDF...", | |||
| "generatingExcel": "生成 Excel...", | |||
| "generatingReport": "生成報告..." | |||
| "generatingReport": "生成報告...", | |||
| "generateError": "產生報告時發生錯誤,請再試一次。", | |||
| "missingRequired": "缺少必填條件:\n- {{fields}}", | |||
| "dateNotBeforeToday": "日期不可早於今天:\n- {{fields}}", | |||
| "selectOrEnterItemCode": "選擇或輸入物料編號", | |||
| "cancel": "取消", | |||
| "confirmDownloadPdf": "確認下載 PDF", | |||
| "confirmDownloadExcel": "確認下載 Excel", | |||
| "semiFgConfirmTitle": "已選擇的物料編號以及列印成品/半成品生產分析報告", | |||
| "semiFgConfirmHint": "請確認以下已選擇的物料編號及其類別:", | |||
| "semiFgColItem": "物料編號及名稱", | |||
| "semiFgColCategory": "類別", | |||
| "qcScopeHelpAll": "匯出全部 QC 檢驗項目(含溫度/濕度)。", | |||
| "qcScopeHelpMeasurable": "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。", | |||
| "categories": { | |||
| "inventory": "庫存管理", | |||
| "inbound-outbound": "出入倉作業", | |||
| "production": "生產與趨勢" | |||
| }, | |||
| "options": { | |||
| "All": "全部", | |||
| "all": "全部", | |||
| "pending": "待盤點", | |||
| "completed": "已審核", | |||
| "success": "成功", | |||
| "failed": "失敗", | |||
| "measurable": "只包含溫度濕度" | |||
| }, | |||
| "reports": { | |||
| "rep-004": { | |||
| "title": "入倉追蹤報告", | |||
| "fields": { | |||
| "lastInDateStart": "入倉日期:由 Last In Date Start", | |||
| "lastInDateEnd": "入倉日期:至 Last In Date End", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-008": { | |||
| "title": "成品出倉報告", | |||
| "fields": { | |||
| "lastOutDateStart": "出貨日期:由 Last Out Date Start", | |||
| "lastOutDateEnd": "出貨日期:至 Last Out Date End", | |||
| "year": "年份 Year", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-012": { | |||
| "title": "庫存盤點報告", | |||
| "fields": { | |||
| "stockTakeRoundId": "盤點輪次(可多選)", | |||
| "itemCode": "貨品編號", | |||
| "store_id": "倉庫樓層", | |||
| "status": "狀態", | |||
| "type": "類型" | |||
| }, | |||
| "options": { | |||
| "store_id": { | |||
| "All": "全部" | |||
| }, | |||
| "status": { | |||
| "All": "全部", | |||
| "pending": "待盤點", | |||
| "completed": "已審核" | |||
| }, | |||
| "type": { | |||
| "All": "全部", | |||
| "PP": "PP", | |||
| "PF": "PF", | |||
| "TOA": "TOA", | |||
| "工廠生產": "工廠生產", | |||
| "倉存調整": "倉存調整", | |||
| "期初存貨": "期初存貨", | |||
| "採購入倉": "採購入倉", | |||
| "其他入倉": "其他入倉" | |||
| } | |||
| } | |||
| }, | |||
| "rep-011": { | |||
| "title": "庫存明細報告", | |||
| "fields": { | |||
| "lastInDateStart": "庫存日期:由 Last In Date Start", | |||
| "lastInDateEnd": "庫存日期:至 Last In Date End", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-007": { | |||
| "title": "庫存結餘報告", | |||
| "fields": { | |||
| "stockDate": "庫存日期: Stock Date", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-014": { | |||
| "title": "PO入倉記錄報告", | |||
| "fields": { | |||
| "receiptDateStart": "收貨日期:由 Receipt Date Start", | |||
| "receiptDateEnd": "收貨日期:至 Receipt Date End", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-009": { | |||
| "title": "成品出倉追蹤報告", | |||
| "fields": { | |||
| "lastOutDateStart": "出貨日期:由 Last Out Date Start", | |||
| "lastOutDateEnd": "出貨日期:至 Last Out Date End", | |||
| "itemCode": "貨品編號 Item Code", | |||
| "handler": "提料員 Handler" | |||
| } | |||
| }, | |||
| "rep-010": { | |||
| "title": "庫存品質檢測報告", | |||
| "fields": { | |||
| "lastInDateStart": "QC 檢測日期:由 QC Date Start", | |||
| "lastInDateEnd": "QC 檢測日期:至 QC Date End", | |||
| "qcType": "QC 類型", | |||
| "itemCode": "貨品編號 Item Code", | |||
| "qcItemScope": "QC 項目範圍" | |||
| }, | |||
| "options": { | |||
| "qcType": { | |||
| "all": "全部", | |||
| "IQC": "IQC(採購)", | |||
| "EPQC": "EPQC(工單)" | |||
| }, | |||
| "qcItemScope": { | |||
| "all": "全部 QC 項目", | |||
| "measurable": "只包含溫度濕度" | |||
| } | |||
| } | |||
| }, | |||
| "rep-013": { | |||
| "title": "貨品出倉追蹤報告", | |||
| "fields": { | |||
| "lastOutDateStart": "出倉日期:由 Last Out Date Start", | |||
| "lastOutDateEnd": "出倉日期:至 Last Out Date End", | |||
| "itemCode": "貨品編號 Item Code", | |||
| "handler": "提料人 Handler" | |||
| } | |||
| }, | |||
| "rep-006": { | |||
| "title": "庫存材料消耗趨勢報告", | |||
| "fields": { | |||
| "lastOutDateStart": "材料消耗日期:由 Last Out Date Start", | |||
| "lastOutDateEnd": "材料消耗日期:至 Last Out Date End", | |||
| "year": "年份 Year", | |||
| "stockCategory": "類別 Category", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-005": { | |||
| "title": "成品/半成品生產分析報告", | |||
| "fields": { | |||
| "lastOutDateStart": "完成生產日期:由 Last Out Date Start", | |||
| "lastOutDateEnd": "完成生產日期:至 Last Out Date End", | |||
| "year": "年份 Year", | |||
| "stockCategory": "類別 Category", | |||
| "itemCode": "貨品編號 Item Code" | |||
| } | |||
| }, | |||
| "rep-015": { | |||
| "title": "M18 BOM Shop 同步記錄", | |||
| "fields": { | |||
| "syncDateStart": "同步日期:由 Sync Date Start", | |||
| "syncDateEnd": "同步日期:至 Sync Date End", | |||
| "finishedItemCode": "成品貨號 Finished Item Code", | |||
| "syncStatus": "同步狀態 Sync Status" | |||
| }, | |||
| "options": { | |||
| "syncStatus": { | |||
| "all": "全部 All", | |||
| "success": "成功 Success", | |||
| "failed": "失敗 Failed" | |||
| } | |||
| } | |||
| }, | |||
| "rep-016": { | |||
| "title": "成品出倉揀貨合規報告", | |||
| "fields": { | |||
| "dateStart": "日期 Date", | |||
| "handler": "提料人 Handler", | |||
| "ticketNo": "提票號碼", | |||
| "itemCode": "貨品編號 Item Code", | |||
| "storeId": "樓層" | |||
| } | |||
| }, | |||
| "rep-017": { | |||
| "title": "店鋪訂單補貨記錄", | |||
| "fields": { | |||
| "shopOrderDateStart": "店鋪訂單日期:由 Shop Order Date Start", | |||
| "shopOrderDateEnd": "店鋪訂單日期:至 Shop Order Date End", | |||
| "shopCode": "店鋪編號 Shop Code" | |||
| } | |||
| }, | |||
| "rep-018": { | |||
| "title": "送貨訂單與倉存單位不符報告", | |||
| "fields": { | |||
| "deliveryDate": "預計送貨日期 Estimated Arrival Date", | |||
| "storeId": "送貨訂單樓層 Floor" | |||
| }, | |||
| "options": { | |||
| "storeId": { | |||
| "All": "全部" | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| "excel": { | |||
| "noData": "(篩選範圍內無資料)", | |||
| "grn": { | |||
| "sheetDetail": "PO入倉記錄", | |||
| "sheetListedPo": "已上架PO金額", | |||
| "noCompletedPo": "(篩選範圍內無已完成之 PO 行)", | |||
| "categoryCurrencyTotal": "貨幣小計", | |||
| "categoryPo": "訂單", | |||
| "poNo": "訂單編號", | |||
| "deliveryNoteNo": "送貨單編號", | |||
| "receiptDate": "收貨日期", | |||
| "itemCode": "物料編號", | |||
| "itemName": "物料名稱", | |||
| "qty": "數量", | |||
| "demandQty": "訂單數量", | |||
| "uom": "單位", | |||
| "supplierLotNo": "供應商批次", | |||
| "expiryDate": "到期日", | |||
| "supplierCode": "供應商編號", | |||
| "supplier": "供應商", | |||
| "status": "入倉狀態", | |||
| "unitPrice": "單價", | |||
| "currency": "貨幣", | |||
| "amount": "金額", | |||
| "grnCode": "M18 入倉單號", | |||
| "grnId": "M18 記錄編號", | |||
| "poCreator": "PO建立者(M18)", | |||
| "note": "備註", | |||
| "category": "類別", | |||
| "totalAmount": "金額", | |||
| "grnCodes": "M18 入倉單號" | |||
| }, | |||
| "bomSync": { | |||
| "sheetSync": "BOM同步記錄", | |||
| "sheetMaterials": "BOM物料明細", | |||
| "syncTime": "同步時間", | |||
| "finishedItemCode": "成品貨號", | |||
| "finishedItemName": "成品名稱", | |||
| "bomRoutingCode": "BOM路由編號", | |||
| "version": "版本", | |||
| "status": "狀態", | |||
| "failureReason": "失敗原因", | |||
| "message": "訊息", | |||
| "lineNo": "行號", | |||
| "materialName": "物料名稱", | |||
| "uom": "單位", | |||
| "qty": "用量", | |||
| "statusSuccess": "成功", | |||
| "statusSkipped": "略過(內容未變)", | |||
| "statusFailed": "失敗" | |||
| }, | |||
| "shopReplenishment": { | |||
| "sheetName": "店鋪訂單補貨記錄", | |||
| "shopCode": "店鋪編號", | |||
| "shopName": "店鋪名稱", | |||
| "shopOrderDate": "店鋪訂單日期", | |||
| "shopOrderNo": "店鋪訂單編號", | |||
| "itemCode": "貨品編號", | |||
| "itemName": "貨品名稱", | |||
| "firstOrderQty": "原訂單數量", | |||
| "firstOrderActualPickQty": "原單實際提料數量", | |||
| "firstOrderPicker": "原單提料人", | |||
| "reorderQty": "補貨數量", | |||
| "reorderDate": "補貨日期", | |||
| "reason": "補貨原因", | |||
| "actualDeliveredQty": "實際補貨數量", | |||
| "actualDeliveredHandler": "實際補貨提料人", | |||
| "deliveredDate": "送貨日期", | |||
| "reasonQuality": "質素問題", | |||
| "reasonOutOfStock": "缺貨", | |||
| "reasonOther": "其他" | |||
| } | |||
| } | |||
| } | |||