Переглянути джерело

added locale to /report

fix負數倉
PC-20260115JRSN\Administrator 3 тижднів тому
джерело
коміт
df0fa53731
10 змінених файлів з 937 додано та 211 видалено
  1. +18
    -12
      src/app/(main)/report/ItemQcReportFilters.tsx
  2. +4
    -2
      src/app/(main)/report/ReportSelectionDashboard.tsx
  3. +15
    -13
      src/app/(main)/report/SemiFGProductionAnalysisReport.tsx
  4. +89
    -57
      src/app/(main)/report/bomShopSyncReportApi.ts
  5. +81
    -52
      src/app/(main)/report/grnReportApi.ts
  6. +38
    -29
      src/app/(main)/report/page.tsx
  7. +47
    -0
      src/app/(main)/report/reportI18n.ts
  8. +85
    -42
      src/app/(main)/report/shopOrderReplenishmentReportApi.ts
  9. +280
    -2
      src/i18n/en/report.json
  10. +280
    -2
      src/i18n/zh/report.json

+ 18
- 12
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 (
<Grid container spacing={3}>
<Grid item {...gridSize}>
<TextField
fullWidth
label="QC 檢測日期:由 QC Date Start"
label={field("lastInDateStart", "QC 檢測日期:由 QC Date Start")}
type="date"
value={criteria.lastInDateStart || ""}
onChange={(e) => onFieldChange("lastInDateStart", e.target.value)}
@@ -43,7 +49,7 @@ export default function ItemQcReportFilters({
<Grid item {...gridSize}>
<TextField
fullWidth
label="QC 檢測日期:至 QC Date End"
label={field("lastInDateEnd", "QC 檢測日期:至 QC Date End")}
type="date"
value={criteria.lastInDateEnd || ""}
onChange={(e) => onFieldChange("lastInDateEnd", e.target.value)}
@@ -54,19 +60,19 @@ export default function ItemQcReportFilters({
<TextField
fullWidth
select
label="QC 類型"
label={field("qcType", "QC 類型")}
value={criteria.qcType || "all"}
onChange={(e) => onFieldChange("qcType", e.target.value)}
>
<MenuItem value="all">全部</MenuItem>
<MenuItem value="IQC">IQC</MenuItem>
<MenuItem value="EPQC">EPQC</MenuItem>
<MenuItem value="all">{opt("qcType", "all", "全部")}</MenuItem>
<MenuItem value="IQC">{opt("qcType", "IQC", "IQC(採購)")}</MenuItem>
<MenuItem value="EPQC">{opt("qcType", "EPQC", "EPQC(工單)")}</MenuItem>
</TextField>
</Grid>
<Grid item {...gridSize}>
<TextField
fullWidth
label="貨品編號 Item Code"
label={field("itemCode", "貨品編號 Item Code")}
value={criteria.itemCode || ""}
onChange={(e) => onFieldChange("itemCode", e.target.value)}
placeholder="e.g. MJ0364"
@@ -76,17 +82,17 @@ export default function ItemQcReportFilters({
<TextField
fullWidth
select
label="QC 項目範圍"
label={field("qcItemScope", "QC 項目範圍")}
value={qcItemFilter === "measurable" ? "measurable" : "all"}
onChange={(e) => onFieldChange("qcItemScope", e.target.value)}
>
<MenuItem value="all">全部 QC 項目</MenuItem>
<MenuItem value="measurable">只包含溫度濕度</MenuItem>
<MenuItem value="all">{opt("qcItemScope", "all", "全部 QC 項目")}</MenuItem>
<MenuItem value="measurable">{opt("qcItemScope", "measurable", "只包含溫度濕度")}</MenuItem>
</TextField>
<FormHelperText>
{qcItemFilter === "measurable"
? "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。"
: "匯出全部 QC 檢驗項目(含溫度/濕度)。"}
? t("qcScopeHelpMeasurable")
: t("qcScopeHelpAll")}
</FormHelperText>
</Grid>
</Grid>


+ 4
- 2
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<string, SvgIconComponent> = {
"rep-011": Inventory2OutlinedIcon,
@@ -116,6 +117,7 @@ function CategoryColumn({
selectedReportId: string;
onSelectReport: (reportId: string) => void;
}) {
const { reportTitle, categoryTitle } = useReportLabels();
const reports = category.reportIds
.map((id) => reportById[id])
.filter(Boolean);
@@ -140,7 +142,7 @@ function CategoryColumn({
}}
>
<Typography variant="subtitle1" fontWeight="bold">
{category.title}
{categoryTitle(category.id, category.title)}
</Typography>
</Box>
<Box
@@ -155,7 +157,7 @@ function CategoryColumn({
<Grid item xs={6} key={report.id}>
<ReportCard
reportId={report.id}
title={report.title}
title={reportTitle(report)}
accent={category.accent}
selected={selectedReportId === report.id}
onClick={() => onSelectReport(report.id)}


+ 15
- 13
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<ItemCodeWithCategory[]>([]);
const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({});
@@ -70,7 +72,7 @@ export default function SemiFGProductionAnalysisReport({
setExportFormat(format);
// Validate required fields
if (requiredFieldLabels.length > 0) {
alert(`缺少必填條件:\n- ${requiredFieldLabels.join('\n- ')}`);
alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') }));
return;
}

@@ -107,7 +109,7 @@ export default function SemiFGProductionAnalysisReport({
setShowConfirmDialog(false);
} catch (error) {
console.error('Failed to generate report:', error);
alert('An error occurred while generating the report. Please try again.');
alert(t('generateError'));
} finally {
setLoading(false);
}
@@ -124,7 +126,7 @@ export default function SemiFGProductionAnalysisReport({
disabled={loading}
sx={{ px: 4 }}
>
{loading ? '生成 PDF...' : '下載報告 (PDF)'}
{loading ? t('generatingPdf') : t('downloadPdf')}
</Button>
<Button
variant="outlined"
@@ -134,7 +136,7 @@ export default function SemiFGProductionAnalysisReport({
disabled={loading}
sx={{ px: 4 }}
>
{loading ? '生成 Excel...' : '下載報告 (Excel)'}
{loading ? t('generatingExcel') : t('downloadExcel')}
</Button>
</div>

@@ -147,22 +149,22 @@ export default function SemiFGProductionAnalysisReport({
>
<DialogTitle>
<Typography variant="h6" fontWeight="bold">
已選擇的物料編號以及列印成品/半成品生產分析報告
{t('semiFgConfirmTitle')}
</Typography>
</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
請確認以下已選擇的物料編號及其類別:
{t('semiFgConfirmHint')}
</Typography>
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>
<strong>物料編號及名稱</strong>
<strong>{t('semiFgColItem')}</strong>
</TableCell>
<TableCell>
<strong>類別</strong>
<strong>{t('semiFgColCategory')}</strong>
</TableCell>
</TableRow>
</TableHead>
@@ -187,7 +189,7 @@ export default function SemiFGProductionAnalysisReport({
</TableContainer>
</DialogContent>
<DialogActions sx={{ p: 2 }}>
<Button onClick={() => setShowConfirmDialog(false)}>取消</Button>
<Button onClick={() => setShowConfirmDialog(false)}>{t('cancel')}</Button>
<Button
variant="contained"
onClick={() => executeExport()}
@@ -196,11 +198,11 @@ export default function SemiFGProductionAnalysisReport({
>
{loading
? exportFormat === 'excel'
? '生成 Excel...'
: '生成 PDF...'
? t('generatingExcel')
: t('generatingPdf')
: exportFormat === 'excel'
? '確認下載 Excel'
: '確認下載 PDF'}
? t('confirmDownloadExcel')
: t('confirmDownloadPdf')}
</Button>
</DialogActions>
</Dialog>


+ 89
- 57
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<string, unknown> {
function emptySyncSheetRow(
L: ReturnType<typeof bomSyncLabels>,
note?: string,
): Record<string, unknown> {
return {
同步時間: note,
成品貨號: "",
成品名稱: "",
BOM路由編號: "",
[L.syncTime]: note ?? L.noData,
[L.finishedItemCode]: "",
[L.finishedItemName]: "",
[L.bomRoutingCode]: "",
"M18 BOM Code": "",
版本: "",
[L.version]: "",
"M18 Record Id": "",
狀態: "",
失敗原因: "",
訊息: "",
[L.status]: "",
[L.failureReason]: "",
[L.message]: "",
"BOM Id": "",
"Sync Log Id": "",
};
}

/** Column keys for sheet 2 — used for headers when there are no data rows. */
function emptyMaterialSheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> {
function emptyMaterialSheetRow(
L: ReturnType<typeof bomSyncLabels>,
note?: string,
): Record<string, unknown> {
return {
同步時間: note,
成品貨號: "",
[L.syncTime]: note ?? L.noData,
[L.finishedItemCode]: "",
"M18 BOM Code": "",
版本: "",
狀態: "",
行號: "",
物料名稱: "",
[L.version]: "",
[L.status]: "",
[L.lineNo]: "",
[L.materialName]: "",
"M18 Product Id": "",
單位: "",
用量: "",
[L.uom]: "",
[L.qty]: "",
"M18 Supplier Id": "",
"M18 Purchase Unit Id": "",
"Sync Log Id": "",
@@ -117,52 +140,59 @@ export async function fetchBomShopSyncReportData(
return (await response.json()) as BomShopSyncReportResponse;
}

function syncStatusLabel(status: string | undefined): string {
function syncStatusLabel(
status: string | undefined,
L: ReturnType<typeof bomSyncLabels>,
): string {
switch (status) {
case "SUCCESS":
return "成功";
return L.statusSuccess;
case "SKIPPED_UNCHANGED":
return "略過(內容未變)";
return L.statusSkipped;
case "FAILED":
return "失敗";
return L.statusFailed;
default:
return status ?? "";
}
}

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

function toMaterialExcelRow(r: BomShopSyncMaterialRow): Record<string, unknown> {
const base = emptyMaterialSheetRow("");
function toMaterialExcelRow(
r: BomShopSyncMaterialRow,
L: ReturnType<typeof bomSyncLabels>,
): Record<string, unknown> {
return {
...base,
同步時間: r.syncDateTime ?? "",
成品貨號: r.finishedItemCode ?? "",
...emptyMaterialSheetRow(L, ""),
[L.syncTime]: r.syncDateTime ?? "",
[L.finishedItemCode]: r.finishedItemCode ?? "",
"M18 BOM Code": r.m18HeaderCode ?? "",
版本: r.version ?? "",
狀態: syncStatusLabel(r.syncStatus),
行號: r.lineNo ?? "",
物料名稱: r.materialName ?? "",
[L.version]: r.version ?? "",
[L.status]: syncStatusLabel(r.syncStatus, L),
[L.lineNo]: r.lineNo ?? "",
[L.materialName]: r.materialName ?? "",
"M18 Product Id": r.udfProductM18Id ?? "",
單位: r.udfBaseUnit ?? "",
用量: r.udfQty ?? "",
[L.uom]: r.udfBaseUnit ?? "",
[L.qty]: r.udfQty ?? "",
"M18 Supplier Id": r.udfSupplierM18Id ?? "",
"M18 Purchase Unit Id": r.udfPurchaseUnitM18Id ?? "",
"Sync Log Id": r.syncLogId ?? "",
@@ -172,16 +202,18 @@ function toMaterialExcelRow(r: BomShopSyncMaterialRow): Record<string, unknown>
export async function generateBomShopSyncReportExcel(
criteria: Record<string, string>,
reportTitle: string = "M18 BOM Shop 同步記錄",
t?: TFunction,
): Promise<void> {
const L = bomSyncLabels(t);
const data = await fetchBomShopSyncReportData(criteria);
const syncRows =
(data.syncRows ?? []).length > 0
? (data.syncRows ?? []).map(toSyncExcelRow)
: [emptySyncSheetRow()];
? (data.syncRows ?? []).map((r) => toSyncExcelRow(r, L))
: [emptySyncSheetRow(L)];
const materialRows =
(data.materialRows ?? []).length > 0
? (data.materialRows ?? []).map(toMaterialExcelRow)
: [emptyMaterialSheetRow()];
? (data.materialRows ?? []).map((r) => toMaterialExcelRow(r, L))
: [emptyMaterialSheetRow(L)];

const start = criteria.syncDateStart;
const end = criteria.syncDateEnd;
@@ -197,8 +229,8 @@ export async function generateBomShopSyncReportExcel(

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


+ 81
- 52
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<string, string | number | undefined> {
const L = grnLabels(t);
const base: Record<string, string | number | undefined> = {
"PO No. / 訂單編號": r.poCode ?? "",
"Delivery Note No. / 送貨單編號": r.deliveryNoteNo ?? "",
"Receipt Date / 收貨日期": r.receiptDate ?? "",
"Item Code / 物料編號": r.itemCode ?? "",
"Item Name / 物料名稱": r.itemName ?? "",
"Qty / 數量": formatQty(
r.acceptedQty ?? r.receivedQty ?? undefined
),
"Demand Qty / 訂單數量": formatQty(r.demandQty),
"UOM / 單位": r.uom ?? r.purchaseUomDesc ?? r.stockUomDesc ?? "",
"Supplier Lot No. 供應商批次": r.productLotNo ?? "",
"Expiry Date / 到期日": r.expiryDate ?? "",
"Supplier Code / 供應商編號": r.supplierCode ?? "",
"Supplier / 供應商": r.supplier ?? "",
"入倉狀態": r.status ?? "",
[L.poNo]: r.poCode ?? "",
[L.deliveryNoteNo]: r.deliveryNoteNo ?? "",
[L.receiptDate]: r.receiptDate ?? "",
[L.itemCode]: r.itemCode ?? "",
[L.itemName]: r.itemName ?? "",
[L.qty]: formatQty(r.acceptedQty ?? r.receivedQty ?? undefined),
[L.demandQty]: formatQty(r.demandQty),
[L.uom]: r.uom ?? r.purchaseUomDesc ?? r.stockUomDesc ?? "",
[L.supplierLotNo]: r.productLotNo ?? "",
[L.expiryDate]: r.expiryDate ?? "",
[L.supplierCode]: r.supplierCode ?? "",
[L.supplier]: r.supplier ?? "",
[L.status]: r.status ?? "",
};
if (includeFinancialColumns) {
base["Unit Price / 單價"] = moneyCellValue(r.unitPrice);
base["Currency / 貨幣"] = r.currencyCode ?? "";
base["Amount / 金額"] = moneyCellValue(r.lineAmount);
base[L.unitPrice] = moneyCellValue(r.unitPrice);
base[L.currency] = r.currencyCode ?? "";
base[L.amount] = moneyCellValue(r.lineAmount);
}
base["GRN Code / M18 入倉單號"] = r.grnCode ?? "";
base["GRN Id / M18 記錄編號"] = r.grnId ?? "";
base["PO建立者(M18) / PO creator (M18)"] = r.poM18CreatorDisplay ?? "";
base[L.grnCode] = r.grnCode ?? "";
base[L.grnId] = r.grnId ?? "";
base[L.poCreator] = r.poM18CreatorDisplay ?? "";
return base;
}

const GRN_SHEET_DETAIL = "PO入倉記錄";
const GRN_SHEET_LISTED_PO = "已上架PO金額";

/** Rows for sheet "已上架PO金額" (ADMIN-only; do not add this sheet for other users). */
function buildListedPoAmountSheetRows(
listed: ListedPoAmounts | undefined
listed: ListedPoAmounts | undefined,
t?: TFunction,
): Record<string, string | number | undefined>[] {
const L = grnLabels(t);
if (
!listed ||
(listed.currencyTotals.length === 0 &&
listed.byPurchaseOrder.length === 0)
) {
return [
{
"Note / 備註":
"(篩選範圍內無已完成之 PO 行) / No completed PO lines in the selected range",
},
];
return [{ [L.note]: L.noCompletedPo }];
}
const out: Record<string, string | number | undefined>[] = [];
for (const c of listed.currencyTotals) {
out.push({
"Category / 類別": "貨幣小計 / Currency total",
"Receipt Date / 收貨日期": c.receiptDate ?? "",
"PO No. / 訂單編號": "",
"Currency / 貨幣": c.currencyCode ?? "",
"Total Amount / 金額": moneyCellValue(c.totalAmount),
"GRN Code(s) / M18 入倉單號": "",
[L.category]: L.categoryCurrencyTotal,
[L.receiptDate]: c.receiptDate ?? "",
[L.poNo]: "",
[L.currency]: c.currencyCode ?? "",
[L.totalAmount]: moneyCellValue(c.totalAmount),
[L.grnCodes]: "",
});
}
for (const p of listed.byPurchaseOrder) {
out.push({
"Category / 類別": "訂單 / PO",
"Receipt Date / 收貨日期": p.receiptDate ?? "",
"PO No. / 訂單編號": p.poCode ?? "",
"Currency / 貨幣": p.currencyCode ?? "",
"Total Amount / 金額": moneyCellValue(p.totalAmount),
"GRN Code(s) / M18 入倉單號": p.grnCodes ?? "",
[L.category]: L.categoryPo,
[L.receiptDate]: p.receiptDate ?? "",
[L.poNo]: p.poCode ?? "",
[L.currency]: p.currencyCode ?? "",
[L.totalAmount]: moneyCellValue(p.totalAmount),
[L.grnCodes]: p.grnCodes ?? "",
});
}
return out;
@@ -206,10 +233,11 @@ export async function generateGrnReportExcel(
criteria: Record<string, string>,
reportTitle: string = "PO 入倉記錄",
/** Only users with ADMIN authority should pass true (must match backend). */
includeFinancialColumns: boolean = false
includeFinancialColumns: boolean = false,
t?: TFunction,
): Promise<void> {
const { rows, listedPoAmounts } = await fetchGrnReportData(criteria);
const excelRows = rows.map((r) => toExcelRow(r, includeFinancialColumns));
const excelRows = rows.map((r) => toExcelRow(r, includeFinancialColumns, t));
const start = criteria.receiptDateStart;
const end = criteria.receiptDateEnd;
let datePart: string;
@@ -222,17 +250,18 @@ export async function generateGrnReportExcel(
}
const safeDatePart = datePart.replace(/[^\d\-_/]/g, "");
const filename = `${reportTitle}_${safeDatePart}`;
const L = grnLabels(t);

if (includeFinancialColumns) {
const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts);
const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts, t);
exportMultiSheetToXlsx(
[
{ name: GRN_SHEET_DETAIL, rows: excelRows as Record<string, unknown>[] },
{ name: GRN_SHEET_LISTED_PO, rows: sheet2 as Record<string, unknown>[] },
{ name: L.sheetDetail, rows: excelRows as Record<string, unknown>[] },
{ name: L.sheetListedPo, rows: sheet2 as Record<string, unknown>[] },
],
filename
);
} else {
exportChartToXlsx(excelRows as Record<string, unknown>[], filename, GRN_SHEET_DETAIL);
exportChartToXlsx(excelRows as Record<string, unknown>[], filename, L.sheetDetail);
}
}

+ 38
- 29
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 (
<Box sx={{ p: 4, maxWidth: 1280, margin: '0 auto' }}>
<Typography variant="h4" gutterBottom fontWeight="bold">
報告管理
{t('title')}
</Typography>

<ReportSelectionDashboard
@@ -448,15 +453,19 @@ export default function ReportPage() {
<Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}>
<CardContent>
<Typography variant="h6" color="primary" gutterBottom>
搜索條件: {currentReport.title}
{t('searchCriteriaWithTitle', { title: reportTitle(currentReport) })}
</Typography>
<Divider sx={{ mb: 3 }} />

<Grid container spacing={3}>
{currentReport.fields.map((field) => {
const translatedLabel = fieldLabel(currentReport.id, field);
const options = field.dynamicOptions
? (dynamicOptions[field.name] || [])
: (field.options || []);
: (field.options || []).map((opt) => ({
...opt,
label: optionLabel(currentReport.id, field.name, opt),
}));
const currentValue = criteria[field.name] || '';
const valueForSelect = field.multiple
? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : [])
@@ -491,7 +500,7 @@ export default function ReportPage() {
}
/>
}
label={field.label}
label={translatedLabel}
/>
</Grid>
);
@@ -547,8 +556,8 @@ export default function ReportPage() {
<TextField
{...params}
fullWidth
label={field.label}
placeholder={field.placeholder || "選擇或輸入物料編號"}
label={translatedLabel}
placeholder={field.placeholder || t('selectOrEnterItemCode')}
sx={currentReport.id === 'rep-005' ? {
'& .MuiOutlinedInput-root': {
minHeight: '64px',
@@ -590,7 +599,7 @@ export default function ReportPage() {
<Grid item {...gridSize} key={field.name}>
<TextField
fullWidth
label={field.label}
label={translatedLabel}
type={field.type}
placeholder={field.placeholder}
disabled={disabledByCheckedCheckbox || disabledRep012Status}
@@ -683,10 +692,10 @@ export default function ReportPage() {
{currentReport.id === 'rep-005' ? (
<SemiFGProductionAnalysisReport
criteria={criteria}
requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => f.label)}
requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => fieldLabel(currentReport.id, f))}
loading={loading}
setLoading={setLoading}
reportTitle={currentReport.title}
reportTitle={reportTitle(currentReport)}
onExportSuccess={(format) => {
logFeatureUsage(
FEATURE_USAGE.REPORT_MANAGEMENT,
@@ -705,7 +714,7 @@ export default function ReportPage() {
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成 PDF..." : "下載報告 (PDF)"}
{loading ? t('generatingPdf') : t('downloadPdf')}
</Button>
<Button
variant="outlined"
@@ -715,7 +724,7 @@ export default function ReportPage() {
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成 Excel..." : "下載報告 (Excel)"}
{loading ? t('generatingExcel') : t('downloadExcel')}
</Button>
</>
) : currentReport.id === 'rep-006' || currentReport.id === 'rep-010' ? (
@@ -728,7 +737,7 @@ export default function ReportPage() {
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成 PDF..." : "下載報告 (PDF)"}
{loading ? t('generatingPdf') : t('downloadPdf')}
</Button>
<Button
variant="outlined"
@@ -738,7 +747,7 @@ export default function ReportPage() {
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成 Excel..." : "下載報告 (Excel)"}
{loading ? t('generatingExcel') : t('downloadExcel')}
</Button>
</>
) : currentReport.responseType === 'excel' ? (
@@ -750,7 +759,7 @@ export default function ReportPage() {
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成 Excel..." : "下載報告 (Excel)"}
{loading ? t('generatingExcel') : t('downloadExcel')}
</Button>
) : (
<Button
@@ -761,7 +770,7 @@ export default function ReportPage() {
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成報告..." : "下載報告 (PDF)"}
{loading ? t('generatingReport') : t('downloadPdf')}
</Button>
)}
</Box>


+ 47
- 0
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<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 }));
}

+ 85
- 42
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<typeof shopReplenishmentLabels>;

function emptySheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> {
function emptySheetRow(
L: ShopReplenishmentLabels,
note?: string,
): Record<string, unknown> {
return {
"店鋪編號": note,
"店鋪名稱": "",
"店鋪訂單日期": "",
"店鋪訂單編號": "",
"貨品編號": "",
"貨品名稱": "",
"原訂單數量": "",
"原單實際提料數量": "",
"原單提料人": "",
"補貨數量": "",
"補貨日期": "",
"補貨原因": "",
"實際補貨數量": "",
"實際補貨提料人": "",
"送貨日期": "",
[L.shopCode]: note ?? L.noData,
[L.shopName]: "",
[L.shopOrderDate]: "",
[L.shopOrderNo]: "",
[L.itemCode]: "",
[L.itemName]: "",
[L.firstOrderQty]: "",
[L.firstOrderActualPickQty]: "",
[L.firstOrderPicker]: "",
[L.reorderQty]: "",
[L.reorderDate]: "",
[L.reason]: "",
[L.actualDeliveredQty]: "",
[L.actualDeliveredHandler]: "",
[L.deliveredDate]: "",
};
}

function formatReason(reason: string | undefined): string {
function formatReason(
reason: string | undefined,
L: ShopReplenishmentLabels,
): string {
switch ((reason ?? "").trim()) {
case "quality_issue":
return "質素問題";
return L.reasonQuality;
case "out_of_stock":
return "缺貨";
return L.reasonOutOfStock;
case "other":
return "其他";
return L.reasonOther;
default:
return reason ?? "";
}
@@ -91,25 +130,27 @@ function formatQty(value: unknown): string | number {
return n;
}

function toExcelRow(r: ShopOrderReplenishmentReportRow): Record<string, unknown> {
const base = emptySheetRow("");
function toExcelRow(
r: ShopOrderReplenishmentReportRow,
L: ShopReplenishmentLabels,
): Record<string, unknown> {
return {
...base,
"店鋪編號": r.shopNo ?? "",
"店鋪名稱": r.shopName ?? "",
"店鋪訂單日期": formatDateCell(r.shopOrderDate),
"店鋪訂單編號": r.shopOrderNo ?? "",
"貨品編號": r.itemNo ?? "",
"貨品名稱": r.itemName ?? "",
"原訂單數量": formatQty(r.firstOrderQty),
"原單實際提料數量": formatQty(r.firstOrderActualPickQty),
"原單提料人": r.firstOrderPickerHandler ?? "",
"補貨數量": formatQty(r.reorderQty),
"補貨日期": formatDateCell(r.reorderDate),
"補貨原因": formatReason(r.reason),
"實際補貨數量": formatQty(r.actualDeliveredQty),
"實際補貨提料人": r.actualDeliveredHandler ?? "",
"送貨日期": formatDateCell(r.deliveredDate),
...emptySheetRow(L, ""),
[L.shopCode]: r.shopNo ?? "",
[L.shopName]: r.shopName ?? "",
[L.shopOrderDate]: formatDateCell(r.shopOrderDate),
[L.shopOrderNo]: r.shopOrderNo ?? "",
[L.itemCode]: r.itemNo ?? "",
[L.itemName]: r.itemName ?? "",
[L.firstOrderQty]: formatQty(r.firstOrderQty),
[L.firstOrderActualPickQty]: formatQty(r.firstOrderActualPickQty),
[L.firstOrderPicker]: r.firstOrderPickerHandler ?? "",
[L.reorderQty]: formatQty(r.reorderQty),
[L.reorderDate]: formatDateCell(r.reorderDate),
[L.reason]: formatReason(r.reason, L),
[L.actualDeliveredQty]: formatQty(r.actualDeliveredQty),
[L.actualDeliveredHandler]: r.actualDeliveredHandler ?? "",
[L.deliveredDate]: formatDateCell(r.deliveredDate),
};
}

@@ -143,10 +184,12 @@ export async function fetchShopOrderReplenishmentReportData(
export async function generateShopOrderReplenishmentReportExcel(
criteria: Record<string, string>,
reportTitle: string = "店鋪訂單補貨記錄",
t?: TFunction,
): Promise<void> {
const L = shopReplenishmentLabels(t);
const rows = await fetchShopOrderReplenishmentReportData(criteria);
const excelRows =
rows.length > 0 ? rows.map(toExcelRow) : [emptySheetRow()];
rows.length > 0 ? rows.map((r) => toExcelRow(r, L)) : [emptySheetRow(L)];

const dateCandidates = [
criteria.reorderDateStart,
@@ -163,5 +206,5 @@ export async function generateShopOrderReplenishmentReportExcel(
const safeDatePart = datePart.replace(/[^\d\-_/]/g, "");
const filename = `${reportTitle}_${safeDatePart}`;

exportChartToXlsx(excelRows, filename, SHEET_NAME);
exportChartToXlsx(excelRows, filename, L.sheetName);
}

+ 280
- 2
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"
}
}
}

+ 280
- 2
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": "其他"
}
}
}

Завантаження…
Відмінити
Зберегти