Ver código fonte

no message

reset-do-picking-order
PC-20260115JRSN\Administrator 4 dias atrás
pai
commit
67ee15b312
5 arquivos alterados com 230 adições e 3 exclusões
  1. +21
    -0
      src/app/(main)/chart/_components/exportChartToXlsx.ts
  2. +59
    -0
      src/app/(main)/report/GRN_REPORT_BACKEND_SPEC.md
  3. +99
    -0
      src/app/(main)/report/grnReportApi.ts
  4. +36
    -3
      src/app/(main)/report/page.tsx
  5. +15
    -0
      src/config/reportConfig.ts

+ 21
- 0
src/app/(main)/chart/_components/exportChartToXlsx.ts Ver arquivo

@@ -19,6 +19,27 @@ export function exportChartToXlsx(
return;
}
const ws = XLSX.utils.json_to_sheet(rows);

// Auto-set column widths based on header length (simple heuristic).
const header = Object.keys(rows[0] ?? {});
if (header.length > 0) {
ws["!cols"] = header.map((h) => ({
// Basic width: header length + padding, minimum 12
wch: Math.max(12, h.length + 4),
}));

// Make header row look like a header (bold).
header.forEach((_, colIdx) => {
const cellRef = XLSX.utils.encode_cell({ r: 0, c: colIdx });
const cell = ws[cellRef];
if (cell) {
cell.s = {
font: { bold: true },
};
}
});
}

const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, sheetName);
XLSX.writeFile(wb, `${filename}.xlsx`);


+ 59
- 0
src/app/(main)/report/GRN_REPORT_BACKEND_SPEC.md Ver arquivo

@@ -0,0 +1,59 @@
# GRN Report – Backend API Spec

The frontend **GRN/入倉明細報告** report calls the following endpoint. The backend must implement it to return JSON (not PDF).

## Endpoint

- **Method:** `GET`
- **Path:** `/report/grn-report`
- **Query parameters (all optional):**
- `receiptDateStart` – date (e.g. `yyyy-MM-dd`), filter receipt date from
- `receiptDateEnd` – date (e.g. `yyyy-MM-dd`), filter receipt date to
- `itemCode` – string, filter by item code (partial match if desired)

## Response

- **Content-Type:** `application/json`
- **Body:** Either an array of row objects, or an object with a `rows` array:

```json
{
"rows": [
{
"poCode": "PO-2025-001",
"deliveryNoteNo": "DN-12345",
"receiptDate": "2025-03-15",
"itemCode": "MAT-001",
"itemName": "Raw Material A",
"acceptedQty": 100,
"receivedQty": 100,
"demandQty": 120,
"uom": "KG",
"purchaseUomDesc": "Kilogram",
"stockUomDesc": "KG",
"productLotNo": "LOT-001",
"expiryDate": "2026-03-01",
"supplier": "Supplier Name",
"status": "completed"
}
]
}
```

Or a direct array:

```json
[
{ "poCode": "PO-2025-001", "deliveryNoteNo": "DN-12345", ... }
]
```

## Suggested backend implementation

- Use data that “generates the GRN” (Goods Received Note): e.g. **stock-in lines** (or equivalent) linked to **PO** and **delivery note**.
- Filter by:
- `receiptDate` (or equivalent) between `receiptDateStart` and `receiptDateEnd` when provided.
- `itemCode` when provided.
- Return one row per GRN line with at least: **PO/delivery note no.**, **itemCode**, **itemName**, **qty** (e.g. `acceptedQty`), **uom**, and optionally receipt date, lot, expiry, supplier, status.

Frontend builds the Excel from this JSON and downloads it with columns: PO No., Delivery Note No., Receipt Date, Item Code, Item Name, Qty, Demand Qty, UOM, Product Lot No., Expiry Date, Supplier, Status.

+ 99
- 0
src/app/(main)/report/grnReportApi.ts Ver arquivo

@@ -0,0 +1,99 @@
"use client";

import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { exportChartToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx";

export interface GrnReportRow {
poCode?: string;
deliveryNoteNo?: string;
receiptDate?: string;
itemCode?: string;
itemName?: string;
acceptedQty?: number;
receivedQty?: number;
demandQty?: number;
uom?: string;
purchaseUomDesc?: string;
stockUomDesc?: string;
productLotNo?: string;
expiryDate?: string;
supplierCode?: string;
supplier?: string;
status?: string;
grnId?: number | string;
[key: string]: unknown;
}

export interface GrnReportResponse {
rows: GrnReportRow[];
}

/**
* Fetch GRN (Goods Received Note) report data by date range.
* Backend: GET /report/grn-report?receiptDateStart=&receiptDateEnd=&itemCode=
*/
export async function fetchGrnReportData(
criteria: Record<string, string>
): Promise<GrnReportRow[]> {
const queryParams = new URLSearchParams(criteria).toString();
const url = `${NEXT_PUBLIC_API_URL}/report/grn-report?${queryParams}`;

const response = await clientAuthFetch(url, {
method: "GET",
headers: { Accept: "application/json" },
});

if (response.status === 401 || response.status === 403)
throw new Error("Unauthorized");
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);

const data = (await response.json()) as GrnReportResponse | GrnReportRow[];
const rows = Array.isArray(data) ? data : (data as GrnReportResponse).rows ?? [];
return rows;
}

/** Excel column headers (bilingual) for GRN report */
function toExcelRow(r: GrnReportRow): Record<string, string | number | undefined> {
return {
"PO No. / 訂單編號": r.poCode ?? "",
"Supplier Code / 供應商編號": r.supplierCode ?? "",
"Delivery Note No. / 送貨單編號": r.deliveryNoteNo ?? "",
"Receipt Date / 收貨日期": r.receiptDate ?? "",
"Item Code / 物料編號": r.itemCode ?? "",
"Item Name / 物料名稱": r.itemName ?? "",
"Qty / 數量": r.acceptedQty ?? r.receivedQty ?? "",
"Demand Qty / 訂單數量": r.demandQty ?? "",
"UOM / 單位": r.uom ?? r.purchaseUomDesc ?? r.stockUomDesc ?? "",
"Product Lot No. / 批次": r.productLotNo ?? "",
"Expiry Date / 到期日": r.expiryDate ?? "",
"Supplier / 供應商": r.supplier ?? "",
"Status / 狀態": r.status ?? "",
"GRN Id / M18 單號": r.grnId ?? "",
};
}

/**
* Generate and download GRN report as Excel.
*/
export async function generateGrnReportExcel(
criteria: Record<string, string>,
reportTitle: string = "PO 入倉記錄"
): Promise<void> {
const rows = await fetchGrnReportData(criteria);
const excelRows = rows.map(toExcelRow);
const start = criteria.receiptDateStart;
const end = criteria.receiptDateEnd;
let datePart: string;
if (start && end && start === end) {
datePart = start;
} else if (start || end) {
datePart = `${start || ""}_to_${end || ""}`;
} else {
datePart = new Date().toISOString().slice(0, 10);
}
const safeDatePart = datePart.replace(/[^\d\-_/]/g, "");
const filename = `${reportTitle}_${safeDatePart}`;
exportChartToXlsx(excelRows, filename, "GRN");
}

+ 36
- 3
src/app/(main)/report/page.tsx Ver arquivo

@@ -23,6 +23,7 @@ import {
fetchSemiFGItemCodes,
fetchSemiFGItemCodesWithCategory
} from './semiFGProductionAnalysisApi';
import { generateGrnReportExcel } from './grnReportApi';

interface ItemCodeWithName {
code: string;
@@ -144,9 +145,30 @@ export default function ReportPage() {
}

// For rep-005, the print logic is handled by SemiFGProductionAnalysisReport component
// For other reports, execute print directly
if (currentReport.id !== 'rep-005') {
await executePrint();
if (currentReport.id === 'rep-005') return;

// For Excel reports (e.g. GRN), fetch JSON and download as .xlsx
if (currentReport.responseType === 'excel') {
await executeExcelReport();
return;
}

await executePrint();
};

const executeExcelReport = async () => {
if (!currentReport) return;
setLoading(true);
try {
if (currentReport.id === 'rep-014') {
await generateGrnReportExcel(criteria, currentReport.title);
}
setShowConfirmDialog(false);
} catch (error) {
console.error("Failed to generate Excel report:", error);
alert("An error occurred while generating the report. Please try again.");
} finally {
setLoading(false);
}
};

@@ -425,6 +447,17 @@ export default function ReportPage() {
setLoading={setLoading}
reportTitle={currentReport.title}
/>
) : currentReport.responseType === 'excel' ? (
<Button
variant="contained"
size="large"
startIcon={<PrintIcon />}
onClick={handlePrint}
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "生成報告..." : "匯出 Excel"}
</Button>
) : (
<Button
variant="contained"


+ 15
- 0
src/config/reportConfig.ts Ver arquivo

@@ -16,10 +16,14 @@ export interface ReportField {
allowInput?: boolean; // Allow user to input custom values (for select types)
}

export type ReportResponseType = 'pdf' | 'excel';

export interface ReportDefinition {
id: string;
title: string;
apiEndpoint: string;
/** When 'excel', report page fetches JSON and builds .xlsx for download. Default 'pdf'. */
responseType?: ReportResponseType;
fields: ReportField[];
}

@@ -186,6 +190,17 @@ export const REPORTS: ReportDefinition[] = [
]
},

{
id: "rep-014",
title: "PO 入倉記錄",
apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/grn-report`,
responseType: "excel",
fields: [
{ label: "收貨日期:由 Receipt Date Start", name: "receiptDateStart", type: "date", required: false },
{ label: "收貨日期:至 Receipt Date End", name: "receiptDateEnd", type: "date", required: false },
{ label: "物料編號 Item Code", name: "itemCode", type: "text", required: false },
],
},
{
id: "rep-005",
title: "成品/半成品生產分析報告",


Carregando…
Cancelar
Salvar