"use client"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { exportChartToXlsx, exportMultiSheetToXlsx, } 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; /** PO line unit price (purchase_order_line.up) */ unitPrice?: number; /** unitPrice × acceptedQty */ lineAmount?: number; /** PO currency code (currency.code) */ currencyCode?: string; /** M18 AN document code from m18_goods_receipt_note_log.grn_code */ grnCode?: string; /** M18 record id (m18_record_id) */ grnId?: number | string; [key: string]: unknown; } /** Sheet "已上架PO金額": totals grouped by receipt date + currency / PO (ADMIN-only data from API). */ export interface ListedPoAmounts { currencyTotals: { receiptDate?: string; currencyCode?: string; totalAmount?: number; }[]; byPurchaseOrder: { receiptDate?: string; poCode?: string; currencyCode?: string; totalAmount?: number; grnCodes?: string; }[]; } export interface GrnReportResponse { rows: GrnReportRow[]; listedPoAmounts?: ListedPoAmounts; } /** * Fetch GRN (Goods Received Note) report data by date range. * Backend: GET /report/grn-report?receiptDateStart=&receiptDateEnd=&itemCode= */ export async function fetchGrnReportData( criteria: Record ): Promise<{ rows: GrnReportRow[]; listedPoAmounts?: ListedPoAmounts }> { 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[]; if (Array.isArray(data)) { return { rows: data }; } const body = data as GrnReportResponse; return { rows: body.rows ?? [], listedPoAmounts: body.listedPoAmounts, }; } /** Coerce API JSON (number or numeric string) to a finite number. */ function coerceToFiniteNumber(value: unknown): number | null { if (value === null || value === undefined) return null; if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string") { const t = value.trim(); if (t === "") return null; const n = Number(t); return Number.isFinite(n) ? n : null; } return null; } /** * Cell value for money columns: numeric when possible so Excel export can apply `#,##0.00` (see exportChartToXlsx). */ function moneyCellValue(v: unknown): number | string { const n = coerceToFiniteNumber(v); if (n === null) return ""; return n; } /** Thousands separator for quantities (up to 4 decimal places, trims trailing zeros). */ const formatQty = (n: number | undefined | null): string => { if (n === undefined || n === null || Number.isNaN(Number(n))) return ""; return new Intl.NumberFormat("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 4, }).format(Number(n)); }; /** Excel column headers (bilingual) for GRN report */ function toExcelRow( r: GrnReportRow, includeFinancialColumns: boolean ): Record { const base: Record = { "PO No. / 訂單編號": r.poCode ?? "", "Delivery Note No. / 送貨單編號": r.deliveryNoteNo ?? "", "Receipt Date / 收貨日期": r.receiptDate ?? "", "Item Code / 物料編號": r.itemCode ?? "", "Item Name / 物料名稱": r.itemName ?? "", "Qty / 數量": formatQty( r.acceptedQty ?? r.receivedQty ?? undefined ), "Demand Qty / 訂單數量": formatQty(r.demandQty), "UOM / 單位": r.uom ?? r.purchaseUomDesc ?? r.stockUomDesc ?? "", "Supplier Lot No. 供應商批次": r.productLotNo ?? "", "Expiry Date / 到期日": r.expiryDate ?? "", "Supplier Code / 供應商編號": r.supplierCode ?? "", "Supplier / 供應商": r.supplier ?? "", "入倉狀態": r.status ?? "", }; if (includeFinancialColumns) { base["Unit Price / 單價"] = moneyCellValue(r.unitPrice); base["Currency / 貨幣"] = r.currencyCode ?? ""; base["Amount / 金額"] = moneyCellValue(r.lineAmount); } base["GRN Code / M18 入倉單號"] = r.grnCode ?? ""; base["GRN Id / M18 記錄編號"] = r.grnId ?? ""; 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 ): Record[] { if ( !listed || (listed.currencyTotals.length === 0 && listed.byPurchaseOrder.length === 0) ) { return [ { "Note / 備註": "(篩選範圍內無已完成之 PO 行) / No completed PO lines in the selected range", }, ]; } const out: Record[] = []; for (const c of listed.currencyTotals) { out.push({ "Category / 類別": "貨幣小計 / Currency total", "Receipt Date / 收貨日期": c.receiptDate ?? "", "PO No. / 訂單編號": "", "Currency / 貨幣": c.currencyCode ?? "", "Total Amount / 金額": moneyCellValue(c.totalAmount), "GRN Code(s) / M18 入倉單號": "", }); } 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 ?? "", }); } return out; } /** * Generate and download GRN report as Excel. * Sheet "已上架PO金額" is included only when `includeFinancialColumns` is true (ADMIN). */ export async function generateGrnReportExcel( criteria: Record, reportTitle: string = "PO 入倉記錄", /** Only users with ADMIN authority should pass true (must match backend). */ includeFinancialColumns: boolean = false ): Promise { const { rows, listedPoAmounts } = await fetchGrnReportData(criteria); const excelRows = rows.map((r) => toExcelRow(r, includeFinancialColumns)); 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}`; if (includeFinancialColumns) { const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts); exportMultiSheetToXlsx( [ { name: GRN_SHEET_DETAIL, rows: excelRows as Record[] }, { name: GRN_SHEET_LISTED_PO, rows: sheet2 as Record[] }, ], filename ); } else { exportChartToXlsx(excelRows as Record[], filename, GRN_SHEET_DETAIL); } }