|
- "use client";
-
- import { NEXT_PUBLIC_API_URL } from "@/config/api";
- import { buildItemCodePasteRequestBody } from './parseItemCodeTokens';
- import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
- 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;
- 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;
- /** From purchase_order.m18CreatedUId; e.g. "2569 (legato)" */
- poM18CreatorDisplay?: 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: POST /report/grn-report { receiptDateStart, receiptDateEnd, itemCodes }
- */
- export async function fetchGrnReportData(
- criteria: Record<string, string>
- ): Promise<{ rows: GrnReportRow[]; listedPoAmounts?: ListedPoAmounts }> {
- const url = `${NEXT_PUBLIC_API_URL}/report/grn-report`;
-
- const response = await clientAuthFetch(url, {
- method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- },
- body: JSON.stringify(buildItemCodePasteRequestBody(criteria, "itemCode")),
- });
-
- 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));
- };
-
- 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,
- t?: TFunction,
- ): Record<string, string | number | undefined> {
- const L = grnLabels(t);
- const base: Record<string, string | number | undefined> = {
- [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[L.unitPrice] = moneyCellValue(r.unitPrice);
- base[L.currency] = r.currencyCode ?? "";
- base[L.amount] = moneyCellValue(r.lineAmount);
- }
- base[L.grnCode] = r.grnCode ?? "";
- base[L.grnId] = r.grnId ?? "";
- base[L.poCreator] = r.poM18CreatorDisplay ?? "";
- return base;
- }
-
- function buildListedPoAmountSheetRows(
- 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 [{ [L.note]: L.noCompletedPo }];
- }
- const out: Record<string, string | number | undefined>[] = [];
- for (const c of listed.currencyTotals) {
- out.push({
- [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({
- [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;
- }
-
- /**
- * 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,
- t?: TFunction,
- ): Promise<void> {
- const { rows, listedPoAmounts } = await fetchGrnReportData(criteria);
- const excelRows = rows.map((r) => toExcelRow(r, includeFinancialColumns, t));
- 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}`;
- const L = grnLabels(t);
-
- if (includeFinancialColumns) {
- const sheet2 = buildListedPoAmountSheetRows(listedPoAmounts, t);
- exportMultiSheetToXlsx(
- [
- { 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, L.sheetDetail);
- }
- }
|