|
- "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<string, string>
- ): 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<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 ?? "",
- };
- 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<string, string | number | undefined>[] {
- if (
- !listed ||
- (listed.currencyTotals.length === 0 &&
- listed.byPurchaseOrder.length === 0)
- ) {
- return [
- {
- "Note / 備註":
- "(篩選範圍內無已完成之 PO 行) / No completed PO lines in the selected range",
- },
- ];
- }
- 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 入倉單號": "",
- });
- }
- 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<string, string>,
- reportTitle: string = "PO 入倉記錄",
- /** Only users with ADMIN authority should pass true (must match backend). */
- includeFinancialColumns: boolean = false
- ): Promise<void> {
- 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<string, unknown>[] },
- { name: GRN_SHEET_LISTED_PO, rows: sheet2 as Record<string, unknown>[] },
- ],
- filename
- );
- } else {
- exportChartToXlsx(excelRows as Record<string, unknown>[], filename, GRN_SHEET_DETAIL);
- }
- }
|