|
- import type { TFunction } from "i18next";
- import type { ItemLotTraceResponse } from "@/app/api/itemTracing";
- import {
- exportMultiSheetToXlsx,
- type MultiSheetSpec,
- } from "@/app/(main)/chart/_components/exportChartToXlsx";
- import type { CompiledTraceGraph } from "./compileTraceGraph";
- import {
- mergeScopedAdjustments,
- mergeScopedDoDeliveries,
- mergeScopedFailEvents,
- mergeScopedOpenMovements,
- mergeScopedOrigins,
- mergeScopedOutboundUsage,
- mergeScopedPurchaseEvents,
- mergeScopedPutawayEvents,
- mergeScopedQcResults,
- mergeScopedReplenishmentEvents,
- mergeScopedReturnEvents,
- mergeScopedStockTakeEvents,
- mergeScopedTransfers,
- } from "./mergeLocationScopedData";
- import {
- resolveStockTakeAcceptedQty,
- resolveStockTakeBookQty,
- } from "./traceStockTakeUtils";
- import {
- createTraceLabelTranslator,
- type TraceLabelTranslator,
- } from "./traceLabelUtils";
- import { kindLabelKey, phaseLabelKey } from "./traceFlowNodeUtils";
- import { buildTracePresentationRows } from "./tracePresentationAdapter";
- import { isDoGroupChild } from "./traceDoGroupLayout";
- import {
- buildIssueIngredientRows,
- buildIssueStoreRows,
- buildIssueWhereUsedRows,
- formatShopPhone,
- issueStoreItemsLabel,
- issueStoreReason,
- shopNameWithoutCode,
- } from "./buildIssueTraceRows";
-
- const NO_DATA = "(無資料 / No records)";
-
- const HISTORY_SHEET_KEYS = [
- "summary",
- "timeline",
- "origins",
- "qc",
- "warehouse",
- "outbound",
- "stockTake",
- "production",
- ] as const;
-
- const ISSUE_SHEET_KEYS = ["stores", "whereUsed", "ingredients"] as const;
-
- const SHEET_KEYS = [...HISTORY_SHEET_KEYS, ...ISSUE_SHEET_KEYS] as const;
-
- export type ItemLotTraceWorkbook = "history" | "issue";
-
- /** Bilingual workbook text, rewritten to the active site language at export time. */
- const EXCEL_PHRASE: Record<string, string> = {
- [NO_DATA]: "excel.noData",
- "Field / 欄位": "excel.col.field",
- "Value / 值": "excel.col.value",
- "Lot No. / 批號": "excel.summary.lotNo",
- "Item Code / 貨品編號": "excel.col.itemCode",
- "Item Name / 品名": "excel.col.itemName",
- "UOM / 單位": "excel.col.uom",
- "Expiry / 效期": "excel.summary.expiry",
- "Production Date / 生產日期": "excel.summary.productionDate",
- "Stock In Date / 入庫日期": "excel.summary.stockInDate",
- "Total Available / 總可用量": "excel.summary.totalAvailable",
- "BOM Direction / BOM 方向": "excel.summary.bomDirection",
- "Issue Excel / 問題清單": "excel.summary.issueExcel",
- "Job Order / 工單": "excel.col.jobOrder",
- "Plan Start / 計劃開工": "excel.summary.planStart",
- "Planned Qty / 計劃產量": "excel.summary.plannedQty",
- "JO Status / 工單狀態": "excel.summary.joStatus",
- "Export Time / 匯出時間": "excel.summary.exportTime",
- "— Warehouse Breakdown / 各倉庫存量 —": "excel.summary.warehouseBreakdown",
- "— Alternate Locations / 其他倉位 —": "excel.summary.alternateLocations",
- "Why / 原因": "excel.col.why",
- "Times Received / 收貨次數": "excel.col.timesReceived",
- "Items / 收到的貨": "excel.col.itemsReceived",
- "Latest / 最近一次": "excel.col.latest",
- "Level / 層級": "excel.col.level",
- "Shop Code / 門市編號": "excel.col.shopCode",
- "Shop Name / 門市名稱": "excel.col.shopName",
- "Contact / 聯絡人": "excel.col.contact",
- "Phone / 電話": "excel.col.phone",
- "Email / 電郵": "excel.col.email",
- "Address / 地址": "excel.col.address",
- "District / 地區": "excel.col.district",
- "Lot / 批號": "excel.col.lot",
- "Qty / 數量": "excel.col.qty",
- "Delivery Order / 送貨單": "excel.col.deliveryOrder",
- "Delivery Note / DN": "excel.col.deliveryNote",
- "Ticket / 票號": "excel.col.ticket",
- "Date/Time / 時間": "excel.col.dateTime",
- "Handler / 經手人": "excel.col.handler",
- "Consumed Item Code / 被用貨品編號": "excel.col.consumedItemCode",
- "Consumed Item / 被用品名": "excel.col.consumedItem",
- "Consumed Lot / 被用批號": "excel.col.consumedLot",
- "Qty Used / 用量": "excel.col.qtyUsed",
- "Produced Item Code / 製成品編號": "excel.col.producedItemCode",
- "Produced Item / 製成品名": "excel.col.producedItem",
- "Produced Lot / 製成批號": "excel.col.producedLot",
- "Produced Qty / 製成數量": "excel.col.producedQty",
- "Kind / 類型": "excel.col.kind",
- "Qty Per Unit / 每單位用量": "excel.col.qtyPerUnit",
- "Pick Order / 提料單": "excel.col.pickOrder",
- "Assigned Step / 對應製程": "excel.col.assignedStep",
- "Picked At / 提料時間": "excel.col.pickedAt",
- "Phase / 階段": "excel.col.phase",
- "Event Type / 事件類型": "excel.col.eventType",
- "Doc No. / 單號": "excel.col.docNo",
- "Ref Type / 來源類型": "excel.col.refType",
- "Ref Type Code": "excel.col.refTypeCode",
- "Warehouse / 倉位": "excel.col.warehouse",
- "Remarks / 備註": "excel.col.remarks",
- "Section / 區塊": "excel.col.section",
- "Inventory Lot Id": "excel.col.inventoryLotId",
- "Type / 類型": "excel.col.type",
- "Type Code": "excel.col.typeCode",
- "Supplier Code / 供應商編號": "excel.col.supplierCode",
- "Supplier / 供應商": "excel.col.supplier",
- "DN No. / 送貨單號": "excel.col.dnNo",
- "Status / 狀態": "excel.col.status",
- "Date / 日期": "excel.col.date",
- "QC Result / 結果": "excel.col.qcResult",
- "QC Type / 品檢類型": "excel.col.qcType",
- "QC Type Code": "excel.col.qcTypeCode",
- "QC Item Code / 品檢項編號": "excel.col.qcItemCode",
- "QC Item / 品檢項": "excel.col.qcItem",
- "Accepted Qty / 抽樣數量": "excel.col.sampleQty",
- "Fail Qty / 不良數量": "excel.col.failQty",
- "Stock In Line Id": "excel.col.stockInLineId",
- "Direction / 方向": "excel.col.direction",
- "From / 來源倉": "excel.col.from",
- "To / 目標倉": "excel.col.to",
- "Usage Type / 類型": "excel.col.usageType",
- "Usage Type Code": "excel.col.usageTypeCode",
- "Conso / 併單": "excel.col.conso",
- "Shop / 門市": "excel.col.shop",
- "Extra / 加單": "excel.col.extra",
- "Replenish / 補貨": "excel.col.replenish",
- "Stock Take Code / 盤點單號": "excel.col.stockTakeCode",
- "Round / 輪次": "excel.col.round",
- "Section / 區域": "excel.col.area",
- "Book Qty / 帳面數量": "excel.col.bookQty",
- "Accepted Qty / 核准數量": "excel.col.approvedQty",
- "Variance Qty / 差異數量": "excel.col.varianceQty",
- "Approver / 核准人": "excel.col.approver",
- "Stock Taker / 初盤人": "excel.col.stockTaker",
- "First Count Qty / 初盤數量": "excel.col.firstCountQty",
- "Second Count Qty / 複盤數量": "excel.col.secondCountQty",
- "Approver Count Qty / 覆盤數量": "excel.col.approverCountQty",
- "Record Status / 紀錄狀態": "excel.col.recordStatus",
- "Step / 步驟": "excel.col.step",
- "Scrap Qty / 損耗數量": "excel.col.scrapQty",
- "Defect Qty / 不良數量": "excel.col.defectQty",
- "Operator / 操作員": "excel.col.operator",
- "Equipment / 設備": "excel.col.equipment",
- "Origin / 來源": "excel.section.origin",
- "Purchase / 採購": "excel.section.purchase",
- "Putaway / 上架": "excel.section.putaway",
- "Transfer / 轉倉": "excel.section.transfer",
- "Adjustment / 庫存調整": "excel.section.adjustment",
- "Opening Stock / 開倉入庫": "excel.section.openingStock",
- "Outbound Usage / 出庫使用": "excel.section.outboundUsage",
- "DO Delivery / 成品出倉": "excel.section.doDelivery",
- "Replenishment / 補貨": "excel.section.replenishment",
- Replenishment: "excel.section.replenishment",
- "Return / 退貨": "excel.section.return",
- "Pick Failure / 揀貨異常": "excel.section.pickFailure",
- "BOM Upstream / BOM 上游": "excel.section.bomUpstream",
- "BOM Downstream / BOM 下游": "excel.section.bomDownstream",
- "BOM Recipe / BOM 配方": "excel.section.bomRecipe",
- "JO Pick / 工單提料": "excel.section.joPick",
- "Production Step / 生產步驟": "excel.section.productionStep",
- "Byproduct / 副產品": "excel.section.byproduct",
- "Lot Relation / 拆批關聯": "excel.section.lotRelation",
- };
-
- const sheetTitle = (t: TFunction, key: (typeof SHEET_KEYS)[number]): string =>
- t(`excel.sheet.${key}`).replace(/[\\/*?:[\]]/g, "").trim().slice(0, 31);
-
- const localizeCell = (value: unknown, t: TFunction): unknown => {
- if (typeof value !== "string") return value;
- const phrase = EXCEL_PHRASE[value];
- return phrase ? t(phrase) : value;
- };
-
- const localizeRows = (
- rows: Record<string, unknown>[],
- t: TFunction,
- ): Record<string, unknown>[] =>
- rows.map((row) => {
- const next: Record<string, unknown> = {};
- Object.entries(row).forEach(([key, value]) => {
- const headerKey = EXCEL_PHRASE[key];
- next[headerKey ? t(headerKey) : key] = localizeCell(value, t);
- });
- return next;
- });
-
- export type ItemLotTraceExportLabels = {
- bomDirectionFg: string;
- bomDirectionMaterial: string;
- bomDirectionUnknown: string;
- };
-
- const orDash = (v: string | number | null | undefined): string | number => {
- if (v == null) return "—";
- if (typeof v === "string" && !v.trim()) return "—";
- return v;
- };
-
- const bomDirectionLabel = (
- direction: string,
- labels: ItemLotTraceExportLabels,
- ): string => {
- const d = direction.trim().toUpperCase();
- if (d === "FINISHED_GOOD") return labels.bomDirectionFg;
- if (d === "MATERIAL") return labels.bomDirectionMaterial;
- return labels.bomDirectionUnknown;
- };
-
- const ensureRows = (
- rows: Record<string, unknown>[],
- emptyTemplate: Record<string, unknown>,
- ): Record<string, unknown>[] => {
- if (rows.length > 0) return rows;
- const firstKey = Object.keys(emptyTemplate)[0];
- return [{ ...emptyTemplate, ...(firstKey ? { [firstKey]: NO_DATA } : {}) }];
- };
-
- const todayYmd = (): string => {
- const d = new Date();
- const y = d.getFullYear();
- const m = String(d.getMonth() + 1).padStart(2, "0");
- const day = String(d.getDate()).padStart(2, "0");
- return `${y}${m}${day}`;
- };
-
- const safeFilenamePart = (s: string): string =>
- s.replace(/[\\/:*?"<>|]/g, "_").trim() || "unknown";
-
- export const buildItemLotTraceFilename = (
- data: ItemLotTraceResponse,
- t: TFunction,
- workbook: ItemLotTraceWorkbook = "history",
- ): string => {
- const item = safeFilenamePart(data.lot.itemCode || "item");
- const lot = safeFilenamePart(data.lot.lotNo || "lot");
- const prefix = safeFilenamePart(
- t(workbook === "issue" ? "excel.issueFilePrefix" : "excel.filePrefix"),
- );
- return `${prefix}_${item}_${lot}_${todayYmd()}`;
- };
-
- const summaryEmpty = (): Record<string, unknown> => ({
- "Field / 欄位": "",
- "Value / 值": "",
- });
-
- const buildSummarySheet = (
- data: ItemLotTraceResponse,
- labels: ItemLotTraceExportLabels,
- exportAt: string,
- t: TFunction,
- ): Record<string, unknown>[] => {
- const tr = createTraceLabelTranslator(t);
- const { lot, warehouseLines, joPrelude, alternateLocations, bomTrace } = data;
- const totalAvailable = warehouseLines.reduce(
- (s, w) => s + (w.availableQty ?? 0),
- 0,
- );
- const rows: Record<string, unknown>[] = [
- { "Field / 欄位": "Lot No. / 批號", "Value / 值": orDash(lot.lotNo) },
- { "Field / 欄位": "Item Code / 貨品編號", "Value / 值": orDash(lot.itemCode) },
- { "Field / 欄位": "Item Name / 品名", "Value / 值": orDash(lot.itemName) },
- { "Field / 欄位": "UOM / 單位", "Value / 值": orDash(lot.uom) },
- { "Field / 欄位": "Expiry / 效期", "Value / 值": orDash(lot.expiryDate) },
- {
- "Field / 欄位": "Production Date / 生產日期",
- "Value / 值": orDash(lot.productionDate),
- },
- {
- "Field / 欄位": "Stock In Date / 入庫日期",
- "Value / 值": orDash(lot.stockInDate),
- },
- {
- "Field / 欄位": "Total Available / 總可用量",
- "Value / 值": totalAvailable,
- },
- {
- "Field / 欄位": "BOM Direction / BOM 方向",
- "Value / 值": bomDirectionLabel(bomTrace.direction, labels),
- },
- {
- "Field / 欄位": "Issue Excel / 問題清單",
- "Value / 值": t("excel.summary.issueExcelNote"),
- },
- ];
-
- if (joPrelude) {
- const jo = joPrelude.jobOrder;
- rows.push(
- {
- "Field / 欄位": "Job Order / 工單",
- "Value / 值": orDash(jo.jobOrderCode),
- },
- {
- "Field / 欄位": "Plan Start / 計劃開工",
- "Value / 值": orDash(jo.planStart),
- },
- { "Field / 欄位": "Planned Qty / 計劃產量", "Value / 值": jo.reqQty },
- {
- "Field / 欄位": "JO Status / 工單狀態",
- "Value / 值": orDash(tr.joStatus(jo.status)),
- },
- );
- }
-
- rows.push(
- { "Field / 欄位": "Export Time / 匯出時間", "Value / 值": exportAt },
-
- { "Field / 欄位": "", "Value / 值": "" },
- {
- "Field / 欄位": "— Warehouse Breakdown / 各倉庫存量 —",
- "Value / 值": "",
- },
- );
-
- warehouseLines.forEach((w) => {
- rows.push({
- "Field / 欄位": t("excel.summary.warehouseLine", { code: w.warehouseCode }),
- "Value / 值": t("excel.summary.warehouseQty", {
- inQty: w.inQty,
- outQty: w.outQty,
- available: w.availableQty,
- status: w.status ? tr.lotLineStatus(w.status) : "—",
- }),
- });
- });
-
- if (alternateLocations.length > 0) {
- rows.push(
- { "Field / 欄位": "", "Value / 值": "" },
- {
- "Field / 欄位": "— Alternate Locations / 其他倉位 —",
- "Value / 值": "",
- },
- );
- alternateLocations.forEach((loc) => {
- rows.push({
- "Field / 欄位": t("excel.summary.inventoryLot", { id: loc.inventoryLotId }),
- "Value / 值": t("excel.summary.locationQty", {
- code: loc.warehouseCode,
- qty: loc.availableQty,
- }),
- });
- });
- }
-
- return rows.length > 0 ? rows : [summaryEmpty()];
- };
-
- const storesEmpty = (): Record<string, unknown> => ({
- "Shop Code / 門市編號": "",
- "Shop Name / 門市名稱": "",
- "Phone / 電話": "",
- "Address / 地址": "",
- "District / 地區": "",
- "Times Received / 收貨次數": "",
- "Qty / 數量": "",
- "UOM / 單位": "",
- "Why / 原因": "",
- "Items / 收到的貨": "",
- "Delivery Order / 送貨單": "",
- "Latest / 最近一次": "",
- });
-
- const buildStoresSheet = (
- data: ItemLotTraceResponse,
- t: TFunction,
- ): Record<string, unknown>[] => {
- const rows = buildIssueStoreRows(data).map((row) => {
- const singleQty = row.qtyByUom.length === 1 ? row.qtyByUom[0] : null;
- return {
- "Shop Code / 門市編號": orDash(row.shopCode),
- "Shop Name / 門市名稱": orDash(shopNameWithoutCode(row.shopCode, row.shopName)),
- "Phone / 電話": formatShopPhone(row.contactNo),
- "Address / 地址": orDash(row.address),
- "District / 地區": orDash(row.district),
- "Times Received / 收貨次數": row.receiptCount,
- // One line per unit so "11" and "1包x2磅" stay paired instead of colliding.
- "Qty / 數量": singleQty
- ? singleQty.qty
- : row.qtyByUom.map((part) => part.qty).join("\n") || "—",
- "UOM / 單位": singleQty
- ? orDash(singleQty.uom)
- : row.qtyByUom.map((part) => part.uom || "—").join("\n") || "—",
- "Why / 原因": issueStoreReason(row, t),
- "Items / 收到的貨": issueStoreItemsLabel(row),
- "Delivery Order / 送貨單": orDash(row.deliveryOrderCodes.join("\n")),
- "Latest / 最近一次": orDash(row.latestTimestamp),
- };
- });
- return ensureRows(rows, storesEmpty());
- };
-
- const whereUsedEmpty = (): Record<string, unknown> => ({
- "Level / 層級": "",
- "Consumed Item Code / 被用貨品編號": "",
- "Consumed Item / 被用品名": "",
- "Consumed Lot / 被用批號": "",
- "Qty Used / 用量": "",
- "Job Order / 工單": "",
- "Produced Item Code / 製成品編號": "",
- "Produced Item / 製成品名": "",
- "Produced Lot / 製成批號": "",
- "Produced Qty / 製成數量": "",
- "UOM / 單位": "",
- });
-
- const buildWhereUsedSheet = (
- data: ItemLotTraceResponse,
- ): Record<string, unknown>[] => {
- const rows = buildIssueWhereUsedRows(data).map((row) => ({
- "Level / 層級": row.depth,
- "Consumed Item Code / 被用貨品編號": orDash(row.consumedItemCode),
- "Consumed Item / 被用品名": orDash(row.consumedItemName),
- "Consumed Lot / 被用批號": orDash(row.consumedLotNo),
- "Qty Used / 用量": row.materialQtyUsed,
- "Job Order / 工單": orDash(row.jobOrderCode),
- "Produced Item Code / 製成品編號": orDash(row.producedItemCode),
- "Produced Item / 製成品名": orDash(row.producedItemName),
- "Produced Lot / 製成批號": orDash(row.producedLotNo),
- "Produced Qty / 製成數量": row.producedQty,
- "UOM / 單位": orDash(row.producedUom),
- }));
- return ensureRows(rows, whereUsedEmpty());
- };
-
- const ingredientsEmpty = (): Record<string, unknown> => ({
- "Kind / 類型": "",
- "Item Code / 貨品編號": "",
- "Item Name / 品名": "",
- "Lot / 批號": "",
- "Qty / 數量": "",
- "UOM / 單位": "",
- "Qty Per Unit / 每單位用量": "",
- "Job Order / 工單": "",
- "Pick Order / 提料單": "",
- "Assigned Step / 對應製程": "",
- "Picked At / 提料時間": "",
- });
-
- const buildIngredientsSheet = (
- data: ItemLotTraceResponse,
- t: TFunction,
- ): Record<string, unknown>[] => {
- const rows = buildIssueIngredientRows(data).map((row) => ({
- "Kind / 類型":
- row.kind === "RECIPE" ? t("issueIngredientRecipe") : t("issueIngredientActual"),
- "Item Code / 貨品編號": orDash(row.itemCode),
- "Item Name / 品名": orDash(row.itemName),
- "Lot / 批號": orDash(row.lotNo),
- "Qty / 數量": row.qty ?? "",
- "UOM / 單位": orDash(row.uom),
- "Qty Per Unit / 每單位用量": row.qtyPerUnit ?? "",
- "Job Order / 工單": orDash(row.jobOrderCode),
- "Pick Order / 提料單": orDash(row.pickOrderCode),
- "Assigned Step / 對應製程": orDash(row.assignedStepName),
- "Picked At / 提料時間": orDash(row.pickedAt),
- }));
- return ensureRows(rows, ingredientsEmpty());
- };
-
- const timelineEmpty = (): Record<string, unknown> => ({
- "Date/Time / 時間": "",
- "Phase / 階段": "",
- "Event Type / 事件類型": "",
- "Doc No. / 單號": "",
- "Ref Type / 來源類型": "",
- "Ref Type Code": "",
- "Qty / 數量": "",
- "UOM / 單位": "",
- "Warehouse / 倉位": "",
- "Handler / 經手人": "",
- "Lot / 批號": "",
- "Remarks / 備註": "",
- });
-
- const buildTimelineSheet = (
- compiledGraph: CompiledTraceGraph,
- t: TFunction,
- tr: TraceLabelTranslator,
- ): Record<string, unknown>[] => {
- const nodes = compiledGraph.layout.nodes
- .filter(
- (n) =>
- n.kind !== "DO_GROUP" && n.kind !== "PICK_GROUP" && !isDoGroupChild(n),
- )
- .slice()
- .sort((a, b) => {
- const ta = a.timestamp ?? "";
- const tb = b.timestamp ?? "";
- if (ta !== tb) return ta.localeCompare(tb);
- return a.sortKey - b.sortKey || a.sequenceIndex - b.sequenceIndex;
- });
-
- const rows = nodes.map((n) => {
- const handlerDetail = n.details.find(
- (d) =>
- d.label.toLowerCase().includes("handler") ||
- d.label.includes("經手") ||
- d.label.includes("核准") ||
- d.label.includes("盤點"),
- );
- return {
- "Date/Time / 時間": orDash(n.timestamp),
- "Phase / 階段": t(phaseLabelKey(n.phase)),
- "Event Type / 事件類型": t(kindLabelKey(n.kind)),
- "Doc No. / 單號": orDash(n.refCode),
- "Ref Type / 來源類型": tr.refType(n.refType),
- "Ref Type Code": orDash(n.refType),
- "Qty / 數量": n.qty ?? "",
- "UOM / 單位": orDash(n.uom),
- "Warehouse / 倉位": orDash(n.warehouseCode),
- "Handler / 經手人": orDash(
- handlerDetail?.value ?? n.meta?.split(" · ")[0],
- ),
- "Lot / 批號": orDash(n.traceLotNo),
- "Remarks / 備註": orDash(n.subtitle),
- };
- });
- return ensureRows(rows, timelineEmpty());
- };
-
- const originsEmpty = (): Record<string, unknown> => ({
- "Section / 區塊": "",
- "Warehouse / 倉位": "",
- "Inventory Lot Id": "",
- "Type / 類型": "",
- "Type Code": "",
- "Doc No. / 單號": "",
- "Supplier Code / 供應商編號": "",
- "Supplier / 供應商": "",
- "DN No. / 送貨單號": "",
- "Qty / 數量": "",
- "Status / 狀態": "",
- "Date / 日期": "",
- });
-
- const buildOriginsSheet = (
- data: ItemLotTraceResponse,
- tr: TraceLabelTranslator,
- ): Record<string, unknown>[] => {
- const originRows = mergeScopedOrigins(data).map((o) => ({
- "Section / 區塊": "Origin / 來源",
- "Warehouse / 倉位": o.scopeWarehouseCode,
- "Inventory Lot Id": o.inventoryLotId,
- "Type / 類型": tr.refType(o.type),
- "Type Code": orDash(o.type),
- "Doc No. / 單號": orDash(o.refCode),
- "Supplier Code / 供應商編號": orDash(o.supplierCode),
- "Supplier / 供應商": orDash(o.supplierName),
- "DN No. / 送貨單號": orDash(o.dnNo),
- "Qty / 數量": o.acceptedQty,
- "Status / 狀態": tr.stockInStatus(o.status),
- "Date / 日期": orDash(o.receiptDate),
- }));
-
- const purchaseRows = mergeScopedPurchaseEvents(data).map((p) => ({
- "Section / 區塊": "Purchase / 採購",
- "Warehouse / 倉位": p.scopeWarehouseCode,
- "Inventory Lot Id": p.inventoryLotId,
- "Type / 類型": tr.refType("PO"),
- "Type Code": "PO",
- "Doc No. / 單號": orDash(p.purchaseOrderCode),
- "Supplier Code / 供應商編號": orDash(p.supplierCode),
- "Supplier / 供應商": orDash(p.supplierName),
- "DN No. / 送貨單號": "",
- "Qty / 數量": p.orderQty,
- "Status / 狀態": orDash(p.status),
- "Date / 日期": orDash(p.orderDate),
- }));
-
- return ensureRows([...originRows, ...purchaseRows], originsEmpty());
- };
-
- const qcEmpty = (): Record<string, unknown> => ({
- "Warehouse / 倉位": "",
- "Inventory Lot Id": "",
- "QC Result / 結果": "",
- "QC Type / 品檢類型": "",
- "QC Type Code": "",
- "QC Item Code / 品檢項編號": "",
- "QC Item / 品檢項": "",
- "Accepted Qty / 抽樣數量": "",
- "Fail Qty / 不良數量": "",
- "Remarks / 備註": "",
- "Handler / 經手人": "",
- "Date/Time / 時間": "",
- "Stock In Line Id": "",
- });
-
- const buildQcSheet = (
- data: ItemLotTraceResponse,
- tr: TraceLabelTranslator,
- ): Record<string, unknown>[] => {
- const rows = mergeScopedQcResults(data).map((q) => ({
- "Warehouse / 倉位": q.scopeWarehouseCode,
- "Inventory Lot Id": q.inventoryLotId,
- "QC Result / 結果": tr.qcPassed(q.qcPassed),
- "QC Type / 品檢類型": tr.qcType(q.qcType),
- "QC Type Code": orDash(q.qcType),
- "QC Item Code / 品檢項編號": orDash(q.qcItemCode),
- "QC Item / 品檢項": orDash(q.qcItemName || q.qcItemDescription),
- "Accepted Qty / 抽樣數量": q.acceptedQty,
- "Fail Qty / 不良數量": q.failQty,
- "Remarks / 備註": orDash(q.remarks),
- "Handler / 經手人": orDash(q.handledBy),
- "Date/Time / 時間": orDash(q.created),
- "Stock In Line Id": q.stockInLineId,
- }));
- return ensureRows(rows, qcEmpty());
- };
-
- const warehouseEmpty = (): Record<string, unknown> => ({
- "Section / 區塊": "",
- "Warehouse / 倉位": "",
- "Inventory Lot Id": "",
- "Direction / 方向": "",
- "Type / 類型": "",
- "Type Code": "",
- "Doc No. / 單號": "",
- "From / 來源倉": "",
- "To / 目標倉": "",
- "Qty / 數量": "",
- "Handler / 經手人": "",
- "Status / 狀態": "",
- "Remarks / 備註": "",
- "Date/Time / 時間": "",
- });
-
- const buildWarehouseSheet = (
- data: ItemLotTraceResponse,
- tr: TraceLabelTranslator,
- ): Record<string, unknown>[] => {
- const putaway = mergeScopedPutawayEvents(data).map((p) => ({
- "Section / 區塊": "Putaway / 上架",
- "Warehouse / 倉位": p.scopeWarehouseCode,
- "Inventory Lot Id": p.inventoryLotId,
- "Direction / 方向": "",
- "Type / 類型": tr.refType(p.refType),
- "Type Code": orDash(p.refType),
- "Doc No. / 單號": orDash(p.refCode),
- "From / 來源倉": "",
- "To / 目標倉": orDash(p.warehouseCode),
- "Qty / 數量": p.qty,
- "Handler / 經手人": orDash(p.handledBy),
- "Status / 狀態": orDash(p.status),
- "Remarks / 備註": "",
- "Date/Time / 時間": orDash(p.timestamp),
- }));
-
- const transfers = mergeScopedTransfers(data).map((trRow) => ({
- "Section / 區塊": "Transfer / 轉倉",
- "Warehouse / 倉位": trRow.scopeWarehouseCode,
- "Inventory Lot Id": trRow.inventoryLotId,
- "Direction / 方向": "",
- "Type / 類型": tr.refType("TRANSFER"),
- "Type Code": "TRANSFER",
- "Doc No. / 單號": orDash(trRow.transferCode),
- "From / 來源倉": orDash(trRow.fromWarehouse),
- "To / 目標倉": orDash(trRow.toWarehouse),
- "Qty / 數量": trRow.qty,
- "Handler / 經手人": "",
- "Status / 狀態": "",
- "Remarks / 備註": "",
- "Date/Time / 時間": orDash(trRow.timestamp),
- }));
-
- const adjustments = mergeScopedAdjustments(data).map((a) => ({
- "Section / 區塊": "Adjustment / 庫存調整",
- "Warehouse / 倉位": a.scopeWarehouseCode,
- "Inventory Lot Id": a.inventoryLotId,
- "Direction / 方向": tr.direction(a.direction),
- "Type / 類型": tr.adjustmentType(a.adjustmentType),
- "Type Code": orDash(a.adjustmentType),
- "Doc No. / 單號": orDash(a.refCode),
- "From / 來源倉": "",
- "To / 目標倉": orDash(a.warehouseCode),
- "Qty / 數量": a.qty,
- "Handler / 經手人": orDash(a.handledBy),
- "Status / 狀態": "",
- "Remarks / 備註": orDash(a.reason),
- "Date/Time / 時間": orDash(a.timestamp),
- }));
-
- const openMoves = mergeScopedOpenMovements(data).map((o) => ({
- "Section / 區塊": "Opening Stock / 開倉入庫",
- "Warehouse / 倉位": o.scopeWarehouseCode,
- "Inventory Lot Id": o.inventoryLotId,
- "Direction / 方向": tr.direction("IN"),
- "Type / 類型": tr.refType("OPEN"),
- "Type Code": "OPEN",
- "Doc No. / 單號": orDash(o.refCode),
- "From / 來源倉": "",
- "To / 目標倉": orDash(o.warehouseCode),
- "Qty / 數量": o.qty,
- "Handler / 經手人": orDash(o.handledBy),
- "Status / 狀態": "",
- "Remarks / 備註": orDash(o.remarks),
- "Date/Time / 時間": orDash(o.timestamp),
- }));
-
- return ensureRows(
- [...putaway, ...transfers, ...adjustments, ...openMoves],
- warehouseEmpty(),
- );
- };
-
- const outboundEmpty = (): Record<string, unknown> => ({
- "Section / 區塊": "",
- "Warehouse / 倉位": "",
- "Inventory Lot Id": "",
- "Usage Type / 類型": "",
- "Usage Type Code": "",
- "Doc No. / 單號": "",
- "Pick Order / 提料單": "",
- "Conso / 併單": "",
- "Job Order / 工單": "",
- "Delivery Order / 送貨單": "",
- "Delivery Note / DN": "",
- "Ticket / 票號": "",
- "Shop / 門市": "",
- "Qty / 數量": "",
- "Handler / 經手人": "",
- "Extra / 加單": "",
- "Replenish / 補貨": "",
- "Remarks / 備註": "",
- "Date/Time / 時間": "",
- });
-
- const buildOutboundSheet = (
- data: ItemLotTraceResponse,
- tr: TraceLabelTranslator,
- t: TFunction,
- ): Record<string, unknown>[] => {
- const usage = mergeScopedOutboundUsage(data).map((u) => ({
- "Section / 區塊": "Outbound Usage / 出庫使用",
- "Warehouse / 倉位": u.scopeWarehouseCode,
- "Inventory Lot Id": u.inventoryLotId,
- "Usage Type / 類型": tr.usageType(u.usageType),
- "Usage Type Code": orDash(u.usageType),
- "Doc No. / 單號": orDash(u.deliveryOrderCode || u.pickOrderCode),
- "Pick Order / 提料單": orDash(u.pickOrderCode),
- "Conso / 併單": orDash(u.consoCode),
- "Job Order / 工單": orDash(u.jobOrderCode),
- "Delivery Order / 送貨單": orDash(u.deliveryOrderCode),
- "Delivery Note / DN": "",
- "Ticket / 票號": "",
- "Shop / 門市": "",
- "Qty / 數量": u.qty,
- "Handler / 經手人": orDash(u.handler),
- "Extra / 加單": "",
- "Replenish / 補貨": "",
- "Remarks / 備註": "",
- "Date/Time / 時間": orDash(u.timestamp),
- }));
-
- const doRows = mergeScopedDoDeliveries(data).map((d) => ({
- "Section / 區塊": "DO Delivery / 成品出倉",
- "Warehouse / 倉位": d.scopeWarehouseCode,
- "Inventory Lot Id": d.inventoryLotId,
- "Usage Type / 類型": tr.usageType("FG_DELIVERY"),
- "Usage Type Code": "FG_DELIVERY",
- "Doc No. / 單號": orDash(d.deliveryOrderCode),
- "Pick Order / 提料單": orDash(d.pickOrderCode),
- "Conso / 併單": orDash(d.consoCode),
- "Job Order / 工單": "",
- "Delivery Order / 送貨單": orDash(d.deliveryOrderCode),
- "Delivery Note / DN": orDash(d.deliveryNoteCode),
- "Ticket / 票號": orDash(d.ticketNo),
- "Shop / 門市": [d.shopCode, d.shopName].filter(Boolean).join(" · ") || "—",
- "Qty / 數量": d.qty,
- "Handler / 經手人": orDash(d.handler),
- "Extra / 加單": d.isExtra ? t("excel.yes") : "",
- "Replenish / 補貨": d.isReplenish ? t("excel.yes") : "",
- "Remarks / 備註": "",
- "Date/Time / 時間": orDash(d.timestamp),
- }));
-
- const replenish = mergeScopedReplenishmentEvents(data).map((r) => ({
- "Section / 區塊": "Replenishment / 補貨",
- "Warehouse / 倉位": r.scopeWarehouseCode,
- "Inventory Lot Id": r.inventoryLotId,
- "Usage Type / 類型": "Replenishment",
- "Usage Type Code": "REPLENISH",
- "Doc No. / 單號": orDash(r.replenishmentCode),
- "Pick Order / 提料單": "",
- "Conso / 併單": "",
- "Job Order / 工單": "",
- "Delivery Order / 送貨單": orDash(r.sourceDoCode),
- "Delivery Note / DN": "",
- "Ticket / 票號": "",
- "Shop / 門市": [r.shopCode, r.shopName].filter(Boolean).join(" · ") || "—",
- "Qty / 數量": r.replenishQty,
- "Handler / 經手人": orDash(r.handler),
- "Extra / 加單": "",
- "Replenish / 補貨": t("excel.yes"),
- "Remarks / 備註": orDash(r.reason),
- "Date/Time / 時間": orDash(r.timestamp),
- }));
-
- const returns = mergeScopedReturnEvents(data).map((r) => ({
- "Section / 區塊": "Return / 退貨",
- "Warehouse / 倉位": r.scopeWarehouseCode,
- "Inventory Lot Id": r.inventoryLotId,
- "Usage Type / 類型": tr.movementType(r.movementType),
- "Usage Type Code": orDash(r.movementType),
- "Doc No. / 單號": orDash(r.refCode),
- "Pick Order / 提料單": "",
- "Conso / 併單": "",
- "Job Order / 工單": "",
- "Delivery Order / 送貨單": "",
- "Delivery Note / DN": "",
- "Ticket / 票號": "",
- "Shop / 門市": "",
- "Qty / 數量": r.qty,
- "Handler / 經手人": orDash(r.handler),
- "Extra / 加單": "",
- "Replenish / 補貨": "",
- "Remarks / 備註": orDash(r.remarks),
- "Date/Time / 時間": orDash(r.timestamp),
- }));
-
- const fails = mergeScopedFailEvents(data).map((f) => ({
- "Section / 區塊": "Pick Failure / 揀貨異常",
- "Warehouse / 倉位": f.scopeWarehouseCode,
- "Inventory Lot Id": f.inventoryLotId,
- "Usage Type / 類型": tr.failType(f.failType),
- "Usage Type Code": orDash(f.failType),
- "Doc No. / 單號": orDash(f.pickOrderCode),
- "Pick Order / 提料單": orDash(f.pickOrderCode),
- "Conso / 併單": "",
- "Job Order / 工單": "",
- "Delivery Order / 送貨單": "",
- "Delivery Note / DN": "",
- "Ticket / 票號": "",
- "Shop / 門市": "",
- "Qty / 數量": f.qty,
- "Handler / 經手人": orDash(f.handlerName),
- "Extra / 加單": "",
- "Replenish / 補貨": "",
- "Remarks / 備註": orDash(f.category),
- "Date/Time / 時間": orDash(f.recordDate),
- }));
-
- return ensureRows(
- [...usage, ...doRows, ...replenish, ...returns, ...fails],
- outboundEmpty(),
- );
- };
-
- const stockTakeEmpty = (): Record<string, unknown> => ({
- "Warehouse / 倉位": "",
- "Inventory Lot Id": "",
- "Stock Take Code / 盤點單號": "",
- "Round / 輪次": "",
- "Section / 區域": "",
- "Lot / 批號": "",
- "Book Qty / 帳面數量": "",
- "Accepted Qty / 核准數量": "",
- "Variance Qty / 差異數量": "",
- "Approver / 核准人": "",
- "Stock Taker / 初盤人": "",
- "First Count Qty / 初盤數量": "",
- "Second Count Qty / 複盤數量": "",
- "Approver Count Qty / 覆盤數量": "",
- "Record Status / 紀錄狀態": "",
- "Date/Time / 時間": "",
- });
-
- const buildStockTakeSheet = (
- data: ItemLotTraceResponse,
- ): Record<string, unknown>[] => {
- const rows = mergeScopedStockTakeEvents(data).map((e) => {
- const d = e.recordDetail;
- return {
- "Warehouse / 倉位": e.scopeWarehouseCode,
- "Inventory Lot Id": e.inventoryLotId,
- "Stock Take Code / 盤點單號": orDash(e.stockTakeCode),
- "Round / 輪次": orDash(
- e.stockTakeRoundName ??
- (e.stockTakeRoundId != null ? `#${e.stockTakeRoundId}` : ""),
- ),
- "Section / 區域": orDash(e.stockTakeSection),
- "Lot / 批號": orDash(e.lotNo),
- "Book Qty / 帳面數量": resolveStockTakeBookQty(d, e.beforeQty) ?? "",
- "Accepted Qty / 核准數量":
- resolveStockTakeAcceptedQty(d, e.afterQty) ?? "",
- "Variance Qty / 差異數量":
- d?.varianceQty != null ? d.varianceQty : e.varianceQty,
- "Approver / 核准人": orDash(d?.approverName || e.approver),
- "Stock Taker / 初盤人": orDash(d?.stockTakerName),
- "First Count Qty / 初盤數量": d?.pickerFirstQty ?? "",
- "Second Count Qty / 複盤數量": d?.pickerSecondQty ?? "",
- "Approver Count Qty / 覆盤數量": d?.approverQty ?? "",
- "Record Status / 紀錄狀態": orDash(d?.recordStatus),
- "Date/Time / 時間": orDash(e.timestamp),
- };
- });
- return ensureRows(rows, stockTakeEmpty());
- };
-
- const productionEmpty = (): Record<string, unknown> => ({
- "Section / 區塊": "",
- "Job Order / 工單": "",
- "Doc No. / 單號": "",
- "Item Code / 貨品編號": "",
- "Lot / 批號": "",
- "Item Name / 品名": "",
- "Step / 步驟": "",
- "Assigned Step / 對應製程": "",
- "Qty / 數量": "",
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": "",
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": "",
- "Date/Time / 時間": "",
- });
-
- const buildProductionSheet = (
- data: ItemLotTraceResponse,
- compiledGraph: CompiledTraceGraph,
- tr: TraceLabelTranslator,
- ): Record<string, unknown>[] => {
- const rows: Record<string, unknown>[] = [];
- const presentation = buildTracePresentationRows(compiledGraph.nodes, data);
-
- const upstreamSource = data.joPrelude?.materialInputs?.length
- ? data.joPrelude.materialInputs.map((m) => ({
- jobOrderCode: m.jobOrderCode,
- materialItemCode: m.materialItemCode,
- materialLotNo: m.materialLotNo,
- materialQty: m.materialQty,
- materialUom: m.materialUom,
- assignedStepName: m.assignedStepName,
- pickOrderCode: m.pickOrderCode,
- pickedAt: m.pickedAt,
- materialItemName: m.materialItemName,
- }))
- : data.bomTrace.upstream.map((u) => ({
- jobOrderCode: u.jobOrderCode,
- materialItemCode: u.materialItemCode,
- materialLotNo: u.materialLotNo,
- materialQty: u.materialQty,
- materialUom: "",
- assignedStepName: "",
- pickOrderCode: "",
- pickedAt: null as string | null,
- materialItemName: "",
- }));
-
- upstreamSource.forEach((m) => {
- rows.push({
- "Section / 區塊": "BOM Upstream / BOM 上游",
- "Job Order / 工單": orDash(m.jobOrderCode),
- "Doc No. / 單號": orDash(m.pickOrderCode),
- "Item Code / 貨品編號": orDash(m.materialItemCode),
- "Lot / 批號": orDash(m.materialLotNo),
- "Item Name / 品名": orDash(m.materialItemName),
- "Step / 步驟": "",
- "Assigned Step / 對應製程": orDash(m.assignedStepName),
- "Qty / 數量": m.materialQty,
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": orDash(m.materialUom),
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": "",
- "Date/Time / 時間": orDash(m.pickedAt),
- });
- });
-
- data.bomTrace.downstream.forEach((d) => {
- rows.push({
- "Section / 區塊": "BOM Downstream / BOM 下游",
- "Job Order / 工單": orDash(d.jobOrderCode),
- "Doc No. / 單號": "",
- "Item Code / 貨品編號": orDash(d.finishedItemCode),
- "Lot / 批號": orDash(d.finishedLotNo),
- "Item Name / 品名": "",
- "Step / 步驟": "",
- "Assigned Step / 對應製程": "",
- "Qty / 數量": d.fgQty,
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": orDash(d.fgUom),
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": "",
- "Date/Time / 時間": "",
- });
- });
-
- data.bomTrace.bomRecipe.forEach((r) => {
- rows.push({
- "Section / 區塊": "BOM Recipe / BOM 配方",
- "Job Order / 工單": "",
- "Doc No. / 單號": "",
- "Item Code / 貨品編號": orDash(r.materialItemCode),
- "Lot / 批號": "",
- "Item Name / 品名": orDash(r.materialItemName),
- "Step / 步驟": "",
- "Assigned Step / 對應製程": orDash(r.assignedStepName),
- "Qty / 數量": r.qtyPerUnit,
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": orDash(r.uom),
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": "",
- "Date/Time / 時間": "",
- });
- });
-
- presentation.joPicks
- .slice()
- .sort((a, b) => (b.pickedAt ?? "").localeCompare(a.pickedAt ?? ""))
- .forEach((p) => {
- rows.push({
- "Section / 區塊": "JO Pick / 工單提料",
- "Job Order / 工單": "",
- "Doc No. / 單號": orDash(p.pickOrderCode),
- "Item Code / 貨品編號": orDash(p.materialItemCode),
- "Lot / 批號": orDash(p.materialLotNo),
- "Item Name / 品名": "",
- "Step / 步驟": "",
- "Assigned Step / 對應製程": orDash(p.assignedStepName),
- "Qty / 數量": p.qty,
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": orDash(p.uom),
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": "",
- "Date/Time / 時間": orDash(p.pickedAt),
- });
- });
-
- const productionRows =
- data.productionSteps.length > 0
- ? data.productionSteps
- .slice()
- .sort((a, b) => (a.seqNo ?? 0) - (b.seqNo ?? 0))
- .map((s) => ({
- "Section / 區塊": "Production Step / 生產步驟",
- "Job Order / 工單": orDash(data.joPrelude?.jobOrder.jobOrderCode),
- "Doc No. / 單號": "",
- "Item Code / 貨品編號": orDash(data.lot.itemCode),
- "Lot / 批號": orDash(data.lot.lotNo),
- "Item Name / 品名": orDash(data.lot.itemName),
- "Step / 步驟": orDash(s.stepName),
- "Assigned Step / 對應製程": orDash(s.description),
- "Qty / 數量": s.outputQty,
- "Scrap Qty / 損耗數量": s.scrapQty,
- "Defect Qty / 不良數量": s.defectQty,
- "UOM / 單位": orDash(data.lot.uom),
- "Operator / 操作員": orDash(s.operatorName),
- "Equipment / 設備":
- [s.equipmentCode, s.equipmentName].filter(Boolean).join(" · ") ||
- "—",
- "Status / 狀態": tr.productionStatus(s.status),
- "Date/Time / 時間": orDash(s.endTime || s.startTime),
- }))
- : presentation.production
- .slice()
- .sort((a, b) => (a.seqNo ?? 0) - (b.seqNo ?? 0))
- .map((s) => ({
- "Section / 區塊": "Production Step / 生產步驟",
- "Job Order / 工單": orDash(data.joPrelude?.jobOrder.jobOrderCode),
- "Doc No. / 單號": "",
- "Item Code / 貨品編號": orDash(data.lot.itemCode),
- "Lot / 批號": orDash(s.traceLotNo || data.lot.lotNo),
- "Item Name / 品名": orDash(data.lot.itemName),
- "Step / 步驟": orDash(s.stepName),
- "Assigned Step / 對應製程": orDash(s.description),
- "Qty / 數量": s.outputQty,
- "Scrap Qty / 損耗數量": s.scrapQty,
- "Defect Qty / 不良數量": s.defectQty,
- "UOM / 單位": orDash(data.lot.uom),
- "Operator / 操作員": orDash(s.operatorName),
- "Equipment / 設備": orDash(s.equipmentName || s.equipmentCode),
- "Status / 狀態": orDash(s.status),
- "Date/Time / 時間": orDash(s.endTime || s.startTime),
- }));
-
- rows.push(...productionRows);
-
- data.byproductLots.forEach((b) => {
- rows.push({
- "Section / 區塊": "Byproduct / 副產品",
- "Job Order / 工單": orDash(b.jobOrderCode),
- "Doc No. / 單號": "",
- "Item Code / 貨品編號": orDash(b.itemCode),
- "Lot / 批號": orDash(b.lotNo),
- "Item Name / 品名": orDash(b.itemName),
- "Step / 步驟": orDash(b.processStepName),
- "Assigned Step / 對應製程": "",
- "Qty / 數量": b.qty,
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": orDash(b.uom),
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": "",
- "Date/Time / 時間": orDash(b.producedAt),
- });
- });
-
- data.lotRelations.forEach((r) => {
- rows.push({
- "Section / 區塊": "Lot Relation / 拆批關聯",
- "Job Order / 工單": "",
- "Doc No. / 單號": orDash(r.relationType),
- "Item Code / 貨品編號": orDash(r.itemCode),
- "Lot / 批號": orDash(r.lotNo),
- "Item Name / 品名": orDash(r.itemName),
- "Step / 步驟": "",
- "Assigned Step / 對應製程": "",
- "Qty / 數量": r.qty,
- "Scrap Qty / 損耗數量": "",
- "Defect Qty / 不良數量": "",
- "UOM / 單位": "",
- "Operator / 操作員": "",
- "Equipment / 設備": "",
- "Status / 狀態": orDash(r.productLotNo),
- "Date/Time / 時間": orDash(r.timestamp),
- });
- });
-
- return ensureRows(rows, productionEmpty());
- };
-
- /** Build one workbook without triggering download (for tests). */
- export const buildItemLotTraceSheets = (
- data: ItemLotTraceResponse,
- compiledGraph: CompiledTraceGraph,
- t: TFunction,
- exportLabels: ItemLotTraceExportLabels,
- workbook: ItemLotTraceWorkbook = "history",
- exportAt: string = new Date().toISOString().replace("T", " ").slice(0, 19),
- ): MultiSheetSpec[] => {
- const tr = createTraceLabelTranslator(t);
- const built: Record<(typeof SHEET_KEYS)[number], Record<string, unknown>[]> = {
- summary: buildSummarySheet(data, exportLabels, exportAt, t),
- stores: buildStoresSheet(data, t),
- whereUsed: buildWhereUsedSheet(data),
- ingredients: buildIngredientsSheet(data, t),
- timeline: buildTimelineSheet(compiledGraph, t, tr),
- origins: buildOriginsSheet(data, tr),
- qc: buildQcSheet(data, tr),
- warehouse: buildWarehouseSheet(data, tr),
- outbound: buildOutboundSheet(data, tr, t),
- stockTake: buildStockTakeSheet(data),
- production: buildProductionSheet(data, compiledGraph, tr),
- };
- const keys = workbook === "issue" ? ISSUE_SHEET_KEYS : HISTORY_SHEET_KEYS;
- return keys.map((key) => ({
- name: sheetTitle(t, key),
- rows: localizeRows(built[key], t),
- }));
- };
-
- export const itemLotTraceSheetNames = (
- t: TFunction,
- workbook: ItemLotTraceWorkbook = "history",
- ): string[] =>
- (workbook === "issue" ? ISSUE_SHEET_KEYS : HISTORY_SHEET_KEYS).map((key) =>
- sheetTitle(t, key),
- );
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 15 | v1.0.1 | 2026-07-20 */
- export const exportItemLotTraceXlsx = (
- data: ItemLotTraceResponse,
- compiledGraph: CompiledTraceGraph,
- t: TFunction,
- workbook: ItemLotTraceWorkbook = "history",
- ): void => {
- const exportLabels: ItemLotTraceExportLabels = {
- bomDirectionFg: t("bomDirectionFG"),
- bomDirectionMaterial: t("bomDirectionMaterial"),
- bomDirectionUnknown: t("bomDirectionUnknown"),
- };
- const sheets = buildItemLotTraceSheets(
- data,
- compiledGraph,
- t,
- exportLabels,
- workbook,
- );
- exportMultiSheetToXlsx(
- sheets,
- buildItemLotTraceFilename(data, t, workbook),
- );
- };
|