From df0fa5373127c88c31f7138bc92b0c8cdce6b933 Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Wed, 26 Aug 2026 16:21:39 +0800 Subject: [PATCH] added locale to /report --- src/app/(main)/report/ItemQcReportFilters.tsx | 30 +- .../report/ReportSelectionDashboard.tsx | 6 +- .../report/SemiFGProductionAnalysisReport.tsx | 28 +- src/app/(main)/report/bomShopSyncReportApi.ts | 146 +++++---- src/app/(main)/report/grnReportApi.ts | 133 +++++---- src/app/(main)/report/page.tsx | 67 +++-- src/app/(main)/report/reportI18n.ts | 47 +++ .../report/shopOrderReplenishmentReportApi.ts | 127 +++++--- src/i18n/en/report.json | 282 +++++++++++++++++- src/i18n/zh/report.json | 282 +++++++++++++++++- 10 files changed, 937 insertions(+), 211 deletions(-) create mode 100644 src/app/(main)/report/reportI18n.ts diff --git a/src/app/(main)/report/ItemQcReportFilters.tsx b/src/app/(main)/report/ItemQcReportFilters.tsx index 990b4f8b..c50650a3 100644 --- a/src/app/(main)/report/ItemQcReportFilters.tsx +++ b/src/app/(main)/report/ItemQcReportFilters.tsx @@ -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 ( onFieldChange("lastInDateStart", e.target.value)} @@ -43,7 +49,7 @@ export default function ItemQcReportFilters({ onFieldChange("lastInDateEnd", e.target.value)} @@ -54,19 +60,19 @@ export default function ItemQcReportFilters({ onFieldChange("qcType", e.target.value)} > - 全部 - IQC - EPQC + {opt("qcType", "all", "全部")} + {opt("qcType", "IQC", "IQC(採購)")} + {opt("qcType", "EPQC", "EPQC(工單)")} onFieldChange("itemCode", e.target.value)} placeholder="e.g. MJ0364" @@ -76,17 +82,17 @@ export default function ItemQcReportFilters({ onFieldChange("qcItemScope", e.target.value)} > - 全部 QC 項目 - 只包含溫度濕度 + {opt("qcItemScope", "all", "全部 QC 項目")} + {opt("qcItemScope", "measurable", "只包含溫度濕度")} {qcItemFilter === "measurable" - ? "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。" - : "匯出全部 QC 檢驗項目(含溫度/濕度)。"} + ? t("qcScopeHelpMeasurable") + : t("qcScopeHelpAll")} diff --git a/src/app/(main)/report/ReportSelectionDashboard.tsx b/src/app/(main)/report/ReportSelectionDashboard.tsx index 5c7ac845..4b309213 100644 --- a/src/app/(main)/report/ReportSelectionDashboard.tsx +++ b/src/app/(main)/report/ReportSelectionDashboard.tsx @@ -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 = { "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({ }} > - {category.title} + {categoryTitle(category.id, category.title)} onSelectReport(report.id)} diff --git a/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx b/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx index 1bc91169..5592a93c 100644 --- a/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx +++ b/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx @@ -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([]); const [itemCodesWithCategory, setItemCodesWithCategory] = useState>({}); @@ -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')} @@ -147,22 +149,22 @@ export default function SemiFGProductionAnalysisReport({ > - 已選擇的物料編號以及列印成品/半成品生產分析報告 + {t('semiFgConfirmTitle')} - 請確認以下已選擇的物料編號及其類別: + {t('semiFgConfirmHint')} - 物料編號及名稱 + {t('semiFgColItem')} - 類別 + {t('semiFgColCategory')} @@ -187,7 +189,7 @@ export default function SemiFGProductionAnalysisReport({ - + diff --git a/src/app/(main)/report/bomShopSyncReportApi.ts b/src/app/(main)/report/bomShopSyncReportApi.ts index 2af5688b..08b31c48 100644 --- a/src/app/(main)/report/bomShopSyncReportApi.ts +++ b/src/app/(main)/report/bomShopSyncReportApi.ts @@ -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 { +function emptySyncSheetRow( + L: ReturnType, + note?: string, +): Record { 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 { +function emptyMaterialSheetRow( + L: ReturnType, + note?: string, +): Record { 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, +): 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 { - const base = emptySyncSheetRow(""); +function toSyncExcelRow( + r: BomShopSyncRow, + L: ReturnType, +): Record { 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 { - const base = emptyMaterialSheetRow(""); +function toMaterialExcelRow( + r: BomShopSyncMaterialRow, + L: ReturnType, +): Record { 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 export async function generateBomShopSyncReportExcel( criteria: Record, reportTitle: string = "M18 BOM Shop 同步記錄", + t?: TFunction, ): Promise { + 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, ); diff --git a/src/app/(main)/report/grnReportApi.ts b/src/app/(main)/report/grnReportApi.ts index 4e4eec73..a8972cec 100644 --- a/src/app/(main)/report/grnReportApi.ts +++ b/src/app/(main)/report/grnReportApi.ts @@ -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 { + const L = grnLabels(t); const base: Record = { - "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[] { + 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[] = []; 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, reportTitle: string = "PO 入倉記錄", /** Only users with ADMIN authority should pass true (must match backend). */ - includeFinancialColumns: boolean = false + includeFinancialColumns: boolean = false, + t?: TFunction, ): Promise { 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[] }, - { name: GRN_SHEET_LISTED_PO, rows: sheet2 as Record[] }, + { name: L.sheetDetail, rows: excelRows as Record[] }, + { name: L.sheetListedPo, rows: sheet2 as Record[] }, ], filename ); } else { - exportChartToXlsx(excelRows as Record[], filename, GRN_SHEET_DETAIL); + exportChartToXlsx(excelRows as Record[], filename, L.sheetDetail); } } diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx index 1de84bd4..1788945e 100644 --- a/src/app/(main)/report/page.tsx +++ b/src/app/(main)/report/page.tsx @@ -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 ( - 報告管理 + {t('title')} - 搜索條件: {currentReport.title} + {t('searchCriteriaWithTitle', { title: reportTitle(currentReport) })} {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} /> ); @@ -547,8 +556,8 @@ export default function ReportPage() { 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')} ) : 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')} ) : currentReport.responseType === 'excel' ? ( @@ -750,7 +759,7 @@ export default function ReportPage() { disabled={loading} sx={{ px: 4 }} > - {loading ? "生成 Excel..." : "下載報告 (Excel)"} + {loading ? t('generatingExcel') : t('downloadExcel')} ) : ( )} diff --git a/src/app/(main)/report/reportI18n.ts b/src/app/(main)/report/reportI18n.ts new file mode 100644 index 00000000..39b7b606 --- /dev/null +++ b/src/app/(main)/report/reportI18n.ts @@ -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) => + t(`reports.${report.id}.title`, { defaultValue: report.title }); + + const fieldLabel = ( + reportId: string, + field: Pick, + ) => + 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 })); +} diff --git a/src/app/(main)/report/shopOrderReplenishmentReportApi.ts b/src/app/(main)/report/shopOrderReplenishmentReportApi.ts index 9392b25b..eeb2bcd5 100644 --- a/src/app/(main)/report/shopOrderReplenishmentReportApi.ts +++ b/src/app/(main)/report/shopOrderReplenishmentReportApi.ts @@ -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; -function emptySheetRow(note: string = NO_DATA_NOTE): Record { +function emptySheetRow( + L: ShopReplenishmentLabels, + note?: string, +): Record { 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 { - const base = emptySheetRow(""); +function toExcelRow( + r: ShopOrderReplenishmentReportRow, + L: ShopReplenishmentLabels, +): Record { 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, reportTitle: string = "店鋪訂單補貨記錄", + t?: TFunction, ): Promise { + 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); } diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json index c13c6113..502fcd7c 100644 --- a/src/i18n/en/report.json +++ b/src/i18n/en/report.json @@ -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" + } + } } diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json index b3d5b8e0..6fb70669 100644 --- a/src/i18n/zh/report.json +++ b/src/i18n/zh/report.json @@ -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": "其他" + } + } }