|
- 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";
-
- const NO_DATA = "(無資料 / No records)";
-
- const SHEET = {
- summary: "摘要 Summary",
- timeline: "生命週期時間軸 Timeline",
- origins: "來源與採購 Origins",
- qc: "品質檢驗 QC",
- warehouse: "倉儲作業 Warehouse",
- outbound: "出庫 Outbound",
- stockTake: "盤點 Stock Take",
- production: "生產與BOM Production",
- } as const;
-
- 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,
- ): string => {
- const item = safeFilenamePart(data.lot.itemCode || "item");
- const lot = safeFilenamePart(data.lot.lotNo || "lot");
- return `批號追溯_${item}_${lot}_${todayYmd()}`;
- };
-
- const summaryEmpty = (): Record<string, unknown> => ({
- "Field / 欄位": "",
- "Value / 值": "",
- });
-
- const buildSummarySheet = (
- data: ItemLotTraceResponse,
- labels: ItemLotTraceExportLabels,
- exportAt: string,
- ): Record<string, unknown>[] => {
- 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),
- },
- ];
-
- 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(jo.status),
- },
- );
- }
-
- rows.push(
- { "Field / 欄位": "Export Time / 匯出時間", "Value / 值": exportAt },
-
- { "Field / 欄位": "", "Value / 值": "" },
- {
- "Field / 欄位": "— Warehouse Breakdown / 各倉庫存量 —",
- "Value / 值": "",
- },
- );
-
- warehouseLines.forEach((w) => {
- rows.push({
- "Field / 欄位": `Warehouse / 倉位: ${w.warehouseCode}`,
- "Value / 值": `In ${w.inQty} · Out ${w.outQty} · Available ${w.availableQty} · ${w.status}`,
- });
- });
-
- if (alternateLocations.length > 0) {
- rows.push(
- { "Field / 欄位": "", "Value / 值": "" },
- {
- "Field / 欄位": "— Alternate Locations / 其他倉位 —",
- "Value / 值": "",
- },
- );
- alternateLocations.forEach((loc) => {
- rows.push({
- "Field / 欄位": `Inventory Lot ${loc.inventoryLotId}`,
- "Value / 值": `${loc.warehouseCode} · Available ${loc.availableQty}`,
- });
- });
- }
-
- return rows.length > 0 ? rows : [summaryEmpty()];
- };
-
- 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,
- ): 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 ? "Y" : "",
- "Replenish / 補貨": d.isReplenish ? "Y" : "",
- "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 / 補貨": "Y",
- "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 the 8 classified sheets without triggering download (for tests). */
- export const buildItemLotTraceSheets = (
- data: ItemLotTraceResponse,
- compiledGraph: CompiledTraceGraph,
- t: TFunction,
- exportLabels: ItemLotTraceExportLabels,
- exportAt: string = new Date().toISOString().replace("T", " ").slice(0, 19),
- ): MultiSheetSpec[] => {
- const tr = createTraceLabelTranslator(t);
- return [
- {
- name: SHEET.summary,
- rows: buildSummarySheet(data, exportLabels, exportAt),
- },
- { name: SHEET.timeline, rows: buildTimelineSheet(compiledGraph, t, tr) },
- { name: SHEET.origins, rows: buildOriginsSheet(data, tr) },
- { name: SHEET.qc, rows: buildQcSheet(data, tr) },
- { name: SHEET.warehouse, rows: buildWarehouseSheet(data, tr) },
- { name: SHEET.outbound, rows: buildOutboundSheet(data, tr) },
- { name: SHEET.stockTake, rows: buildStockTakeSheet(data) },
- {
- name: SHEET.production,
- rows: buildProductionSheet(data, compiledGraph, tr),
- },
- ];
- };
-
- export const ITEM_LOT_TRACE_SHEET_NAMES = Object.values(SHEET);
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 15 | v1.0.1 | 2026-07-20 */
- export const exportItemLotTraceXlsx = (
- data: ItemLotTraceResponse,
- compiledGraph: CompiledTraceGraph,
- t: TFunction,
- ): void => {
- const exportLabels: ItemLotTraceExportLabels = {
- bomDirectionFg: t("bomDirectionFG"),
- bomDirectionMaterial: t("bomDirectionMaterial"),
- bomDirectionUnknown: t("bomDirectionUnknown"),
- };
- const sheets = buildItemLotTraceSheets(data, compiledGraph, t, exportLabels);
- exportMultiSheetToXlsx(sheets, buildItemLotTraceFilename(data));
- };
|