| @@ -1,6 +1,7 @@ | |||||
| "use client"; | "use client"; | ||||
| import React from "react"; | import React from "react"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | import { | ||||
| FormHelperText, | FormHelperText, | ||||
| Grid, | Grid, | ||||
| @@ -26,14 +27,19 @@ export default function ItemQcReportFilters({ | |||||
| criteria, | criteria, | ||||
| onFieldChange, | onFieldChange, | ||||
| }: ItemQcReportFiltersProps) { | }: ItemQcReportFiltersProps) { | ||||
| const { t } = useTranslation("report"); | |||||
| const qcItemFilter = (criteria.qcItemScope || "all") as QcItemFilter; | 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 ( | return ( | ||||
| <Grid container spacing={3}> | <Grid container spacing={3}> | ||||
| <Grid item {...gridSize}> | <Grid item {...gridSize}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label="QC 檢測日期:由 QC Date Start" | |||||
| label={field("lastInDateStart", "QC 檢測日期:由 QC Date Start")} | |||||
| type="date" | type="date" | ||||
| value={criteria.lastInDateStart || ""} | value={criteria.lastInDateStart || ""} | ||||
| onChange={(e) => onFieldChange("lastInDateStart", e.target.value)} | onChange={(e) => onFieldChange("lastInDateStart", e.target.value)} | ||||
| @@ -43,7 +49,7 @@ export default function ItemQcReportFilters({ | |||||
| <Grid item {...gridSize}> | <Grid item {...gridSize}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label="QC 檢測日期:至 QC Date End" | |||||
| label={field("lastInDateEnd", "QC 檢測日期:至 QC Date End")} | |||||
| type="date" | type="date" | ||||
| value={criteria.lastInDateEnd || ""} | value={criteria.lastInDateEnd || ""} | ||||
| onChange={(e) => onFieldChange("lastInDateEnd", e.target.value)} | onChange={(e) => onFieldChange("lastInDateEnd", e.target.value)} | ||||
| @@ -54,19 +60,19 @@ export default function ItemQcReportFilters({ | |||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| select | select | ||||
| label="QC 類型" | |||||
| label={field("qcType", "QC 類型")} | |||||
| value={criteria.qcType || "all"} | value={criteria.qcType || "all"} | ||||
| onChange={(e) => onFieldChange("qcType", e.target.value)} | 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> | </TextField> | ||||
| </Grid> | </Grid> | ||||
| <Grid item {...gridSize}> | <Grid item {...gridSize}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label="貨品編號 Item Code" | |||||
| label={field("itemCode", "貨品編號 Item Code")} | |||||
| value={criteria.itemCode || ""} | value={criteria.itemCode || ""} | ||||
| onChange={(e) => onFieldChange("itemCode", e.target.value)} | onChange={(e) => onFieldChange("itemCode", e.target.value)} | ||||
| placeholder="e.g. MJ0364" | placeholder="e.g. MJ0364" | ||||
| @@ -76,17 +82,17 @@ export default function ItemQcReportFilters({ | |||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| select | select | ||||
| label="QC 項目範圍" | |||||
| label={field("qcItemScope", "QC 項目範圍")} | |||||
| value={qcItemFilter === "measurable" ? "measurable" : "all"} | value={qcItemFilter === "measurable" ? "measurable" : "all"} | ||||
| onChange={(e) => onFieldChange("qcItemScope", e.target.value)} | 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> | </TextField> | ||||
| <FormHelperText> | <FormHelperText> | ||||
| {qcItemFilter === "measurable" | {qcItemFilter === "measurable" | ||||
| ? "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。" | |||||
| : "匯出全部 QC 檢驗項目(含溫度/濕度)。"} | |||||
| ? t("qcScopeHelpMeasurable") | |||||
| : t("qcScopeHelpAll")} | |||||
| </FormHelperText> | </FormHelperText> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -19,6 +19,7 @@ import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutl | |||||
| import type { SvgIconComponent } from "@mui/icons-material"; | import type { SvgIconComponent } from "@mui/icons-material"; | ||||
| import { REPORTS } from "@/config/reportConfig"; | import { REPORTS } from "@/config/reportConfig"; | ||||
| import { REPORT_CATEGORIES, type ReportCategoryConfig } from "./reportCategories"; | import { REPORT_CATEGORIES, type ReportCategoryConfig } from "./reportCategories"; | ||||
| import { useReportLabels } from "./reportI18n"; | |||||
| const REPORT_ICON_MAP: Record<string, SvgIconComponent> = { | const REPORT_ICON_MAP: Record<string, SvgIconComponent> = { | ||||
| "rep-011": Inventory2OutlinedIcon, | "rep-011": Inventory2OutlinedIcon, | ||||
| @@ -116,6 +117,7 @@ function CategoryColumn({ | |||||
| selectedReportId: string; | selectedReportId: string; | ||||
| onSelectReport: (reportId: string) => void; | onSelectReport: (reportId: string) => void; | ||||
| }) { | }) { | ||||
| const { reportTitle, categoryTitle } = useReportLabels(); | |||||
| const reports = category.reportIds | const reports = category.reportIds | ||||
| .map((id) => reportById[id]) | .map((id) => reportById[id]) | ||||
| .filter(Boolean); | .filter(Boolean); | ||||
| @@ -140,7 +142,7 @@ function CategoryColumn({ | |||||
| }} | }} | ||||
| > | > | ||||
| <Typography variant="subtitle1" fontWeight="bold"> | <Typography variant="subtitle1" fontWeight="bold"> | ||||
| {category.title} | |||||
| {categoryTitle(category.id, category.title)} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| <Box | <Box | ||||
| @@ -155,7 +157,7 @@ function CategoryColumn({ | |||||
| <Grid item xs={6} key={report.id}> | <Grid item xs={6} key={report.id}> | ||||
| <ReportCard | <ReportCard | ||||
| reportId={report.id} | reportId={report.id} | ||||
| title={report.title} | |||||
| title={reportTitle(report)} | |||||
| accent={category.accent} | accent={category.accent} | ||||
| selected={selectedReportId === report.id} | selected={selectedReportId === report.id} | ||||
| onClick={() => onSelectReport(report.id)} | onClick={() => onSelectReport(report.id)} | ||||
| @@ -1,6 +1,7 @@ | |||||
| "use client"; | "use client"; | ||||
| import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | import { | ||||
| Dialog, | Dialog, | ||||
| DialogTitle, | DialogTitle, | ||||
| @@ -43,6 +44,7 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| reportTitle = '成品/半成品生產分析報告', | reportTitle = '成品/半成品生產分析報告', | ||||
| onExportSuccess, | onExportSuccess, | ||||
| }: SemiFGProductionAnalysisReportProps) { | }: SemiFGProductionAnalysisReportProps) { | ||||
| const { t } = useTranslation("report"); | |||||
| const [showConfirmDialog, setShowConfirmDialog] = useState(false); | const [showConfirmDialog, setShowConfirmDialog] = useState(false); | ||||
| const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]); | const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]); | ||||
| const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({}); | const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({}); | ||||
| @@ -70,7 +72,7 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| setExportFormat(format); | setExportFormat(format); | ||||
| // Validate required fields | // Validate required fields | ||||
| if (requiredFieldLabels.length > 0) { | if (requiredFieldLabels.length > 0) { | ||||
| alert(`缺少必填條件:\n- ${requiredFieldLabels.join('\n- ')}`); | |||||
| alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') })); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -107,7 +109,7 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| setShowConfirmDialog(false); | setShowConfirmDialog(false); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error('Failed to generate report:', error); | console.error('Failed to generate report:', error); | ||||
| alert('An error occurred while generating the report. Please try again.'); | |||||
| alert(t('generateError')); | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| @@ -124,7 +126,7 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? '生成 PDF...' : '下載報告 (PDF)'} | |||||
| {loading ? t('generatingPdf') : t('downloadPdf')} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -134,7 +136,7 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? '生成 Excel...' : '下載報告 (Excel)'} | |||||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| @@ -147,22 +149,22 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| > | > | ||||
| <DialogTitle> | <DialogTitle> | ||||
| <Typography variant="h6" fontWeight="bold"> | <Typography variant="h6" fontWeight="bold"> | ||||
| 已選擇的物料編號以及列印成品/半成品生產分析報告 | |||||
| {t('semiFgConfirmTitle')} | |||||
| </Typography> | </Typography> | ||||
| </DialogTitle> | </DialogTitle> | ||||
| <DialogContent> | <DialogContent> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| 請確認以下已選擇的物料編號及其類別: | |||||
| {t('semiFgConfirmHint')} | |||||
| </Typography> | </Typography> | ||||
| <TableContainer component={Paper} variant="outlined"> | <TableContainer component={Paper} variant="outlined"> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell> | <TableCell> | ||||
| <strong>物料編號及名稱</strong> | |||||
| <strong>{t('semiFgColItem')}</strong> | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| <strong>類別</strong> | |||||
| <strong>{t('semiFgColCategory')}</strong> | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| @@ -187,7 +189,7 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| </TableContainer> | </TableContainer> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions sx={{ p: 2 }}> | <DialogActions sx={{ p: 2 }}> | ||||
| <Button onClick={() => setShowConfirmDialog(false)}>取消</Button> | |||||
| <Button onClick={() => setShowConfirmDialog(false)}>{t('cancel')}</Button> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={() => executeExport()} | onClick={() => executeExport()} | ||||
| @@ -196,11 +198,11 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| > | > | ||||
| {loading | {loading | ||||
| ? exportFormat === 'excel' | ? exportFormat === 'excel' | ||||
| ? '生成 Excel...' | |||||
| : '生成 PDF...' | |||||
| ? t('generatingExcel') | |||||
| : t('generatingPdf') | |||||
| : exportFormat === 'excel' | : exportFormat === 'excel' | ||||
| ? '確認下載 Excel' | |||||
| : '確認下載 PDF'} | |||||
| ? t('confirmDownloadExcel') | |||||
| : t('confirmDownloadPdf')} | |||||
| </Button> | </Button> | ||||
| </DialogActions> | </DialogActions> | ||||
| </Dialog> | </Dialog> | ||||
| @@ -5,6 +5,8 @@ import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||||
| import { | import { | ||||
| exportMultiSheetToXlsx, | exportMultiSheetToXlsx, | ||||
| } from "@/app/(main)/chart/_components/exportChartToXlsx"; | } from "@/app/(main)/chart/_components/exportChartToXlsx"; | ||||
| import { reportExcelT as tx } from "./reportI18n"; | |||||
| import type { TFunction } from "i18next"; | |||||
| export interface BomShopSyncReportSummary { | export interface BomShopSyncReportSummary { | ||||
| totalAttempts?: number; | totalAttempts?: number; | ||||
| @@ -55,43 +57,64 @@ export interface BomShopSyncReportResponse { | |||||
| materialRows?: BomShopSyncMaterialRow[]; | 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 { | return { | ||||
| 同步時間: note, | |||||
| 成品貨號: "", | |||||
| 成品名稱: "", | |||||
| BOM路由編號: "", | |||||
| [L.syncTime]: note ?? L.noData, | |||||
| [L.finishedItemCode]: "", | |||||
| [L.finishedItemName]: "", | |||||
| [L.bomRoutingCode]: "", | |||||
| "M18 BOM Code": "", | "M18 BOM Code": "", | ||||
| 版本: "", | |||||
| [L.version]: "", | |||||
| "M18 Record Id": "", | "M18 Record Id": "", | ||||
| 狀態: "", | |||||
| 失敗原因: "", | |||||
| 訊息: "", | |||||
| [L.status]: "", | |||||
| [L.failureReason]: "", | |||||
| [L.message]: "", | |||||
| "BOM Id": "", | "BOM Id": "", | ||||
| "Sync Log 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 { | return { | ||||
| 同步時間: note, | |||||
| 成品貨號: "", | |||||
| [L.syncTime]: note ?? L.noData, | |||||
| [L.finishedItemCode]: "", | |||||
| "M18 BOM Code": "", | "M18 BOM Code": "", | ||||
| 版本: "", | |||||
| 狀態: "", | |||||
| 行號: "", | |||||
| 物料名稱: "", | |||||
| [L.version]: "", | |||||
| [L.status]: "", | |||||
| [L.lineNo]: "", | |||||
| [L.materialName]: "", | |||||
| "M18 Product Id": "", | "M18 Product Id": "", | ||||
| 單位: "", | |||||
| 用量: "", | |||||
| [L.uom]: "", | |||||
| [L.qty]: "", | |||||
| "M18 Supplier Id": "", | "M18 Supplier Id": "", | ||||
| "M18 Purchase Unit Id": "", | "M18 Purchase Unit Id": "", | ||||
| "Sync Log Id": "", | "Sync Log Id": "", | ||||
| @@ -117,52 +140,59 @@ export async function fetchBomShopSyncReportData( | |||||
| return (await response.json()) as BomShopSyncReportResponse; | return (await response.json()) as BomShopSyncReportResponse; | ||||
| } | } | ||||
| function syncStatusLabel(status: string | undefined): string { | |||||
| function syncStatusLabel( | |||||
| status: string | undefined, | |||||
| L: ReturnType<typeof bomSyncLabels>, | |||||
| ): string { | |||||
| switch (status) { | switch (status) { | ||||
| case "SUCCESS": | case "SUCCESS": | ||||
| return "成功"; | |||||
| return L.statusSuccess; | |||||
| case "SKIPPED_UNCHANGED": | case "SKIPPED_UNCHANGED": | ||||
| return "略過(內容未變)"; | |||||
| return L.statusSkipped; | |||||
| case "FAILED": | case "FAILED": | ||||
| return "失敗"; | |||||
| return L.statusFailed; | |||||
| default: | default: | ||||
| return status ?? ""; | return status ?? ""; | ||||
| } | } | ||||
| } | } | ||||
| function toSyncExcelRow(r: BomShopSyncRow): Record<string, unknown> { | |||||
| const base = emptySyncSheetRow(""); | |||||
| function toSyncExcelRow( | |||||
| r: BomShopSyncRow, | |||||
| L: ReturnType<typeof bomSyncLabels>, | |||||
| ): Record<string, unknown> { | |||||
| return { | 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 ?? "", | "M18 BOM Code": r.m18HeaderCode ?? "", | ||||
| 版本: r.version ?? "", | |||||
| [L.version]: r.version ?? "", | |||||
| "M18 Record Id": r.m18RecordId ?? "", | "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 ?? "", | "BOM Id": r.bomId ?? "", | ||||
| "Sync Log Id": r.syncLogId ?? "", | "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 { | return { | ||||
| ...base, | |||||
| 同步時間: r.syncDateTime ?? "", | |||||
| 成品貨號: r.finishedItemCode ?? "", | |||||
| ...emptyMaterialSheetRow(L, ""), | |||||
| [L.syncTime]: r.syncDateTime ?? "", | |||||
| [L.finishedItemCode]: r.finishedItemCode ?? "", | |||||
| "M18 BOM Code": r.m18HeaderCode ?? "", | "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 ?? "", | "M18 Product Id": r.udfProductM18Id ?? "", | ||||
| 單位: r.udfBaseUnit ?? "", | |||||
| 用量: r.udfQty ?? "", | |||||
| [L.uom]: r.udfBaseUnit ?? "", | |||||
| [L.qty]: r.udfQty ?? "", | |||||
| "M18 Supplier Id": r.udfSupplierM18Id ?? "", | "M18 Supplier Id": r.udfSupplierM18Id ?? "", | ||||
| "M18 Purchase Unit Id": r.udfPurchaseUnitM18Id ?? "", | "M18 Purchase Unit Id": r.udfPurchaseUnitM18Id ?? "", | ||||
| "Sync Log Id": r.syncLogId ?? "", | "Sync Log Id": r.syncLogId ?? "", | ||||
| @@ -172,16 +202,18 @@ function toMaterialExcelRow(r: BomShopSyncMaterialRow): Record<string, unknown> | |||||
| export async function generateBomShopSyncReportExcel( | export async function generateBomShopSyncReportExcel( | ||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = "M18 BOM Shop 同步記錄", | reportTitle: string = "M18 BOM Shop 同步記錄", | ||||
| t?: TFunction, | |||||
| ): Promise<void> { | ): Promise<void> { | ||||
| const L = bomSyncLabels(t); | |||||
| const data = await fetchBomShopSyncReportData(criteria); | const data = await fetchBomShopSyncReportData(criteria); | ||||
| const syncRows = | const syncRows = | ||||
| (data.syncRows ?? []).length > 0 | (data.syncRows ?? []).length > 0 | ||||
| ? (data.syncRows ?? []).map(toSyncExcelRow) | |||||
| : [emptySyncSheetRow()]; | |||||
| ? (data.syncRows ?? []).map((r) => toSyncExcelRow(r, L)) | |||||
| : [emptySyncSheetRow(L)]; | |||||
| const materialRows = | const materialRows = | ||||
| (data.materialRows ?? []).length > 0 | (data.materialRows ?? []).length > 0 | ||||
| ? (data.materialRows ?? []).map(toMaterialExcelRow) | |||||
| : [emptyMaterialSheetRow()]; | |||||
| ? (data.materialRows ?? []).map((r) => toMaterialExcelRow(r, L)) | |||||
| : [emptyMaterialSheetRow(L)]; | |||||
| const start = criteria.syncDateStart; | const start = criteria.syncDateStart; | ||||
| const end = criteria.syncDateEnd; | const end = criteria.syncDateEnd; | ||||
| @@ -197,8 +229,8 @@ export async function generateBomShopSyncReportExcel( | |||||
| exportMultiSheetToXlsx( | exportMultiSheetToXlsx( | ||||
| [ | [ | ||||
| { name: SHEET_SYNC, rows: syncRows }, | |||||
| { name: SHEET_MATERIALS, rows: materialRows }, | |||||
| { name: L.sheetSync, rows: syncRows }, | |||||
| { name: L.sheetMaterials, rows: materialRows }, | |||||
| ], | ], | ||||
| filename, | filename, | ||||
| ); | ); | ||||
| @@ -6,6 +6,8 @@ import { | |||||
| exportChartToXlsx, | exportChartToXlsx, | ||||
| exportMultiSheetToXlsx, | exportMultiSheetToXlsx, | ||||
| } from "@/app/(main)/chart/_components/exportChartToXlsx"; | } from "@/app/(main)/chart/_components/exportChartToXlsx"; | ||||
| import { reportExcelT as tx } from "./reportI18n"; | |||||
| import type { TFunction } from "i18next"; | |||||
| export interface GrnReportRow { | export interface GrnReportRow { | ||||
| poCode?: string; | poCode?: string; | ||||
| @@ -122,77 +124,102 @@ const formatQty = (n: number | undefined | null): string => { | |||||
| }).format(Number(n)); | }).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( | function toExcelRow( | ||||
| r: GrnReportRow, | r: GrnReportRow, | ||||
| includeFinancialColumns: boolean | |||||
| includeFinancialColumns: boolean, | |||||
| t?: TFunction, | |||||
| ): Record<string, string | number | undefined> { | ): Record<string, string | number | undefined> { | ||||
| const L = grnLabels(t); | |||||
| const base: Record<string, string | number | undefined> = { | 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) { | 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; | 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( | function buildListedPoAmountSheetRows( | ||||
| listed: ListedPoAmounts | undefined | |||||
| listed: ListedPoAmounts | undefined, | |||||
| t?: TFunction, | |||||
| ): Record<string, string | number | undefined>[] { | ): Record<string, string | number | undefined>[] { | ||||
| const L = grnLabels(t); | |||||
| if ( | if ( | ||||
| !listed || | !listed || | ||||
| (listed.currencyTotals.length === 0 && | (listed.currencyTotals.length === 0 && | ||||
| listed.byPurchaseOrder.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>[] = []; | const out: Record<string, string | number | undefined>[] = []; | ||||
| for (const c of listed.currencyTotals) { | for (const c of listed.currencyTotals) { | ||||
| out.push({ | 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) { | for (const p of listed.byPurchaseOrder) { | ||||
| out.push({ | 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; | return out; | ||||
| @@ -206,10 +233,11 @@ export async function generateGrnReportExcel( | |||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = "PO 入倉記錄", | reportTitle: string = "PO 入倉記錄", | ||||
| /** Only users with ADMIN authority should pass true (must match backend). */ | /** Only users with ADMIN authority should pass true (must match backend). */ | ||||
| includeFinancialColumns: boolean = false | |||||
| includeFinancialColumns: boolean = false, | |||||
| t?: TFunction, | |||||
| ): Promise<void> { | ): Promise<void> { | ||||
| const { rows, listedPoAmounts } = await fetchGrnReportData(criteria); | 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 start = criteria.receiptDateStart; | ||||
| const end = criteria.receiptDateEnd; | const end = criteria.receiptDateEnd; | ||||
| let datePart: string; | let datePart: string; | ||||
| @@ -222,17 +250,18 @@ export async function generateGrnReportExcel( | |||||
| } | } | ||||
| const safeDatePart = datePart.replace(/[^\d\-_/]/g, ""); | const safeDatePart = datePart.replace(/[^\d\-_/]/g, ""); | ||||
| const filename = `${reportTitle}_${safeDatePart}`; | const filename = `${reportTitle}_${safeDatePart}`; | ||||
| const L = grnLabels(t); | |||||
| if (includeFinancialColumns) { | if (includeFinancialColumns) { | ||||
| const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts); | |||||
| const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts, t); | |||||
| exportMultiSheetToXlsx( | 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 | filename | ||||
| ); | ); | ||||
| } else { | } 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, | FormControlLabel, | ||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import DownloadIcon from '@mui/icons-material/Download'; | 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 { NEXT_PUBLIC_API_URL } from '@/config/api'; | ||||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | ||||
| import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | ||||
| import ReportSelectionDashboard from './ReportSelectionDashboard'; | import ReportSelectionDashboard from './ReportSelectionDashboard'; | ||||
| import { useReportLabels } from './reportI18n'; | |||||
| import { | import { | ||||
| fetchSemiFGItemCodes, | fetchSemiFGItemCodes, | ||||
| fetchSemiFGItemCodesWithCategory | fetchSemiFGItemCodesWithCategory | ||||
| @@ -47,6 +48,7 @@ interface ItemCodeWithName { | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */ | ||||
| export default function ReportPage() { | export default function ReportPage() { | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const { t, reportTitle, fieldLabel, optionLabel } = useReportLabels(); | |||||
| const includeGrnFinancialColumns = | const includeGrnFinancialColumns = | ||||
| session?.abilities?.includes(AUTH.ADMIN) ?? false; | session?.abilities?.includes(AUTH.ADMIN) ?? false; | ||||
| @@ -196,7 +198,9 @@ export default function ReportPage() { | |||||
| if (currentReport.id === 'rep-012') { | if (currentReport.id === 'rep-012') { | ||||
| if (rep012RoundIds.length === 0) { | if (rep012RoundIds.length === 0) { | ||||
| alert('缺少必填條件:\n- 盤點輪次'); | |||||
| alert(t('missingRequired', { | |||||
| fields: fieldLabel('rep-012', { name: 'stockTakeRoundId', label: '盤點輪次' }), | |||||
| })); | |||||
| return false; | return false; | ||||
| } | } | ||||
| return true; | return true; | ||||
| @@ -208,10 +212,10 @@ export default function ReportPage() { | |||||
| if (!field.required) return false; | if (!field.required) return false; | ||||
| return !criteria[field.name]; | return !criteria[field.name]; | ||||
| }) | }) | ||||
| .map(field => field.label); | |||||
| .map((field) => fieldLabel(currentReport.id, field)); | |||||
| if (missingFields.length > 0) { | if (missingFields.length > 0) { | ||||
| alert(`缺少必填條件:\n- ${missingFields.join('\n- ')}`); | |||||
| alert(t('missingRequired', { fields: missingFields.join('\n- ') })); | |||||
| return false; | return false; | ||||
| } | } | ||||
| @@ -224,10 +228,10 @@ export default function ReportPage() { | |||||
| const v = (criteria[field.name] || '').trim(); | const v = (criteria[field.name] || '').trim(); | ||||
| return v && v < todayStr; | return v && v < todayStr; | ||||
| }) | }) | ||||
| .map((field) => field.label); | |||||
| .map((field) => fieldLabel(currentReport.id, field)); | |||||
| if (beforeToday.length > 0) { | if (beforeToday.length > 0) { | ||||
| alert(`日期不可早於今天:\n- ${beforeToday.join('\n- ')}`); | |||||
| alert(t('dateNotBeforeToday', { fields: beforeToday.join('\n- ') })); | |||||
| return false; | return false; | ||||
| } | } | ||||
| @@ -304,13 +308,14 @@ export default function ReportPage() { | |||||
| if (currentReport.id === 'rep-014') { | if (currentReport.id === 'rep-014') { | ||||
| await generateGrnReportExcel( | await generateGrnReportExcel( | ||||
| criteria, | criteria, | ||||
| currentReport.title, | |||||
| includeGrnFinancialColumns | |||||
| reportTitle(currentReport), | |||||
| includeGrnFinancialColumns, | |||||
| t, | |||||
| ); | ); | ||||
| } else if (currentReport.id === 'rep-015') { | } else if (currentReport.id === 'rep-015') { | ||||
| await generateBomShopSyncReportExcel(criteria, currentReport.title); | |||||
| await generateBomShopSyncReportExcel(criteria, reportTitle(currentReport), t); | |||||
| } else if (currentReport.id === 'rep-017') { | } else if (currentReport.id === 'rep-017') { | ||||
| await generateShopOrderReplenishmentReportExcel(criteria, currentReport.title); | |||||
| await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t); | |||||
| } else { | } else { | ||||
| // Backend returns actual .xlsx bytes for this Excel endpoint. | // Backend returns actual .xlsx bytes for this Excel endpoint. | ||||
| let queryParams = | let queryParams = | ||||
| @@ -349,7 +354,7 @@ export default function ReportPage() { | |||||
| link.href = downloadUrl; | link.href = downloadUrl; | ||||
| const contentDisposition = response.headers.get('Content-Disposition'); | const contentDisposition = response.headers.get('Content-Disposition'); | ||||
| let fileName = `${currentReport.title}.xlsx`; | |||||
| let fileName = `${reportTitle(currentReport)}.xlsx`; | |||||
| if (contentDisposition?.includes('filename=')) { | if (contentDisposition?.includes('filename=')) { | ||||
| fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); | fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); | ||||
| } | } | ||||
| @@ -370,7 +375,7 @@ export default function ReportPage() { | |||||
| setShowConfirmDialog(false); | setShowConfirmDialog(false); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to generate Excel report:", error); | console.error("Failed to generate Excel report:", error); | ||||
| alert("An error occurred while generating the report. Please try again."); | |||||
| alert(t('generateError')); | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| @@ -407,7 +412,7 @@ export default function ReportPage() { | |||||
| link.href = downloadUrl; | link.href = downloadUrl; | ||||
| const contentDisposition = response.headers.get('Content-Disposition'); | const contentDisposition = response.headers.get('Content-Disposition'); | ||||
| let fileName = `${currentReport.title}.pdf`; | |||||
| let fileName = `${reportTitle(currentReport)}.pdf`; | |||||
| if (contentDisposition?.includes('filename=')) { | if (contentDisposition?.includes('filename=')) { | ||||
| fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); | fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); | ||||
| } | } | ||||
| @@ -427,7 +432,7 @@ export default function ReportPage() { | |||||
| setShowConfirmDialog(false); | setShowConfirmDialog(false); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to generate report:", error); | console.error("Failed to generate report:", error); | ||||
| alert("An error occurred while generating the report. Please try again."); | |||||
| alert(t('generateError')); | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| @@ -436,7 +441,7 @@ export default function ReportPage() { | |||||
| return ( | return ( | ||||
| <Box sx={{ p: 4, maxWidth: 1280, margin: '0 auto' }}> | <Box sx={{ p: 4, maxWidth: 1280, margin: '0 auto' }}> | ||||
| <Typography variant="h4" gutterBottom fontWeight="bold"> | <Typography variant="h4" gutterBottom fontWeight="bold"> | ||||
| 報告管理 | |||||
| {t('title')} | |||||
| </Typography> | </Typography> | ||||
| <ReportSelectionDashboard | <ReportSelectionDashboard | ||||
| @@ -448,15 +453,19 @@ export default function ReportPage() { | |||||
| <Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}> | <Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}> | ||||
| <CardContent> | <CardContent> | ||||
| <Typography variant="h6" color="primary" gutterBottom> | <Typography variant="h6" color="primary" gutterBottom> | ||||
| 搜索條件: {currentReport.title} | |||||
| {t('searchCriteriaWithTitle', { title: reportTitle(currentReport) })} | |||||
| </Typography> | </Typography> | ||||
| <Divider sx={{ mb: 3 }} /> | <Divider sx={{ mb: 3 }} /> | ||||
| <Grid container spacing={3}> | <Grid container spacing={3}> | ||||
| {currentReport.fields.map((field) => { | {currentReport.fields.map((field) => { | ||||
| const translatedLabel = fieldLabel(currentReport.id, field); | |||||
| const options = field.dynamicOptions | const options = field.dynamicOptions | ||||
| ? (dynamicOptions[field.name] || []) | ? (dynamicOptions[field.name] || []) | ||||
| : (field.options || []); | |||||
| : (field.options || []).map((opt) => ({ | |||||
| ...opt, | |||||
| label: optionLabel(currentReport.id, field.name, opt), | |||||
| })); | |||||
| const currentValue = criteria[field.name] || ''; | const currentValue = criteria[field.name] || ''; | ||||
| const valueForSelect = field.multiple | const valueForSelect = field.multiple | ||||
| ? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : []) | ? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : []) | ||||
| @@ -491,7 +500,7 @@ export default function ReportPage() { | |||||
| } | } | ||||
| /> | /> | ||||
| } | } | ||||
| label={field.label} | |||||
| label={translatedLabel} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| @@ -547,8 +556,8 @@ export default function ReportPage() { | |||||
| <TextField | <TextField | ||||
| {...params} | {...params} | ||||
| fullWidth | fullWidth | ||||
| label={field.label} | |||||
| placeholder={field.placeholder || "選擇或輸入物料編號"} | |||||
| label={translatedLabel} | |||||
| placeholder={field.placeholder || t('selectOrEnterItemCode')} | |||||
| sx={currentReport.id === 'rep-005' ? { | sx={currentReport.id === 'rep-005' ? { | ||||
| '& .MuiOutlinedInput-root': { | '& .MuiOutlinedInput-root': { | ||||
| minHeight: '64px', | minHeight: '64px', | ||||
| @@ -590,7 +599,7 @@ export default function ReportPage() { | |||||
| <Grid item {...gridSize} key={field.name}> | <Grid item {...gridSize} key={field.name}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label={field.label} | |||||
| label={translatedLabel} | |||||
| type={field.type} | type={field.type} | ||||
| placeholder={field.placeholder} | placeholder={field.placeholder} | ||||
| disabled={disabledByCheckedCheckbox || disabledRep012Status} | disabled={disabledByCheckedCheckbox || disabledRep012Status} | ||||
| @@ -683,10 +692,10 @@ export default function ReportPage() { | |||||
| {currentReport.id === 'rep-005' ? ( | {currentReport.id === 'rep-005' ? ( | ||||
| <SemiFGProductionAnalysisReport | <SemiFGProductionAnalysisReport | ||||
| criteria={criteria} | 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} | loading={loading} | ||||
| setLoading={setLoading} | setLoading={setLoading} | ||||
| reportTitle={currentReport.title} | |||||
| reportTitle={reportTitle(currentReport)} | |||||
| onExportSuccess={(format) => { | onExportSuccess={(format) => { | ||||
| logFeatureUsage( | logFeatureUsage( | ||||
| FEATURE_USAGE.REPORT_MANAGEMENT, | FEATURE_USAGE.REPORT_MANAGEMENT, | ||||
| @@ -705,7 +714,7 @@ export default function ReportPage() { | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? "生成 PDF..." : "下載報告 (PDF)"} | |||||
| {loading ? t('generatingPdf') : t('downloadPdf')} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -715,7 +724,7 @@ export default function ReportPage() { | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||||
| </Button> | </Button> | ||||
| </> | </> | ||||
| ) : currentReport.id === 'rep-006' || currentReport.id === 'rep-010' ? ( | ) : currentReport.id === 'rep-006' || currentReport.id === 'rep-010' ? ( | ||||
| @@ -728,7 +737,7 @@ export default function ReportPage() { | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? "生成 PDF..." : "下載報告 (PDF)"} | |||||
| {loading ? t('generatingPdf') : t('downloadPdf')} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -738,7 +747,7 @@ export default function ReportPage() { | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||||
| </Button> | </Button> | ||||
| </> | </> | ||||
| ) : currentReport.responseType === 'excel' ? ( | ) : currentReport.responseType === 'excel' ? ( | ||||
| @@ -750,7 +759,7 @@ export default function ReportPage() { | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||||
| {loading ? t('generatingExcel') : t('downloadExcel')} | |||||
| </Button> | </Button> | ||||
| ) : ( | ) : ( | ||||
| <Button | <Button | ||||
| @@ -761,7 +770,7 @@ export default function ReportPage() { | |||||
| disabled={loading} | disabled={loading} | ||||
| sx={{ px: 4 }} | sx={{ px: 4 }} | ||||
| > | > | ||||
| {loading ? "生成報告..." : "下載報告 (PDF)"} | |||||
| {loading ? t('generatingReport') : t('downloadPdf')} | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| </Box> | </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 { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| import { exportChartToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx"; | import { exportChartToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx"; | ||||
| import { reportExcelT as tx } from "./reportI18n"; | |||||
| import type { TFunction } from "i18next"; | |||||
| export interface ShopOrderReplenishmentReportRow { | export interface ShopOrderReplenishmentReportRow { | ||||
| shopNo?: string; | shopNo?: string; | ||||
| @@ -27,38 +29,75 @@ export interface ShopOrderReplenishmentReportResponse { | |||||
| rows: ShopOrderReplenishmentReportRow[]; | 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 { | 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()) { | switch ((reason ?? "").trim()) { | ||||
| case "quality_issue": | case "quality_issue": | ||||
| return "質素問題"; | |||||
| return L.reasonQuality; | |||||
| case "out_of_stock": | case "out_of_stock": | ||||
| return "缺貨"; | |||||
| return L.reasonOutOfStock; | |||||
| case "other": | case "other": | ||||
| return "其他"; | |||||
| return L.reasonOther; | |||||
| default: | default: | ||||
| return reason ?? ""; | return reason ?? ""; | ||||
| } | } | ||||
| @@ -91,25 +130,27 @@ function formatQty(value: unknown): string | number { | |||||
| return n; | return n; | ||||
| } | } | ||||
| function toExcelRow(r: ShopOrderReplenishmentReportRow): Record<string, unknown> { | |||||
| const base = emptySheetRow(""); | |||||
| function toExcelRow( | |||||
| r: ShopOrderReplenishmentReportRow, | |||||
| L: ShopReplenishmentLabels, | |||||
| ): Record<string, unknown> { | |||||
| return { | 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( | export async function generateShopOrderReplenishmentReportExcel( | ||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = "店鋪訂單補貨記錄", | reportTitle: string = "店鋪訂單補貨記錄", | ||||
| t?: TFunction, | |||||
| ): Promise<void> { | ): Promise<void> { | ||||
| const L = shopReplenishmentLabels(t); | |||||
| const rows = await fetchShopOrderReplenishmentReportData(criteria); | const rows = await fetchShopOrderReplenishmentReportData(criteria); | ||||
| const excelRows = | const excelRows = | ||||
| rows.length > 0 ? rows.map(toExcelRow) : [emptySheetRow()]; | |||||
| rows.length > 0 ? rows.map((r) => toExcelRow(r, L)) : [emptySheetRow(L)]; | |||||
| const dateCandidates = [ | const dateCandidates = [ | ||||
| criteria.reorderDateStart, | criteria.reorderDateStart, | ||||
| @@ -163,5 +206,5 @@ export async function generateShopOrderReplenishmentReportExcel( | |||||
| const safeDatePart = datePart.replace(/[^\d\-_/]/g, ""); | const safeDatePart = datePart.replace(/[^\d\-_/]/g, ""); | ||||
| const filename = `${reportTitle}_${safeDatePart}`; | const filename = `${reportTitle}_${safeDatePart}`; | ||||
| exportChartToXlsx(excelRows, filename, SHEET_NAME); | |||||
| exportChartToXlsx(excelRows, filename, L.sheetName); | |||||
| } | } | ||||
| @@ -1,13 +1,291 @@ | |||||
| { | { | ||||
| "Report": "Report", | |||||
| "title": "Report Management", | "title": "Report Management", | ||||
| "selectReport": "Select Report", | "selectReport": "Select Report", | ||||
| "reportList": "Report List", | "reportList": "Report List", | ||||
| "selectReportHelper": "Select a report", | "selectReportHelper": "Select a report", | ||||
| "searchCriteria": "Search Criteria", | "searchCriteria": "Search Criteria", | ||||
| "searchCriteriaWithTitle": "Search Criteria: {{title}}", | |||||
| "downloadPdf": "Download Report (PDF)", | "downloadPdf": "Download Report (PDF)", | ||||
| "downloadExcel": "Download Report (Excel)", | "downloadExcel": "Download Report (Excel)", | ||||
| "generatingPdf": "Generating PDF...", | "generatingPdf": "Generating PDF...", | ||||
| "generatingExcel": "Generating Excel...", | "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": "報告管理", | "title": "報告管理", | ||||
| "selectReport": "選擇報告", | "selectReport": "選擇報告", | ||||
| "reportList": "報告列表", | "reportList": "報告列表", | ||||
| "selectReportHelper": "選擇報告", | "selectReportHelper": "選擇報告", | ||||
| "searchCriteria": "搜索條件", | "searchCriteria": "搜索條件", | ||||
| "searchCriteriaWithTitle": "搜索條件: {{title}}", | |||||
| "downloadPdf": "下載報告 (PDF)", | "downloadPdf": "下載報告 (PDF)", | ||||
| "downloadExcel": "下載報告 (Excel)", | "downloadExcel": "下載報告 (Excel)", | ||||
| "generatingPdf": "生成 PDF...", | "generatingPdf": "生成 PDF...", | ||||
| "generatingExcel": "生成 Excel...", | "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": "其他" | |||||
| } | |||||
| } | |||||
| } | } | ||||