import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; const BASE = `${NEXT_PUBLIC_API_URL}/chart`; function buildParams(params: Record) { const p = new URLSearchParams(); Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== "") p.set(k, String(v)); }); return p.toString(); } export interface StockTransactionsByDateRow { date: string; inQty: number; outQty: number; totalQty: number; } export interface DeliveryOrderByDateRow { date: string; orderCount: number; totalQty: number; } export interface PurchaseOrderByStatusRow { status: string; count: number; } /** Multi-select filters for purchase charts (repeated `supplierId` / `itemCode` / `purchaseOrderNo` query params). */ export type PurchaseOrderChartFilters = { supplierIds?: number[]; itemCodes?: string[]; purchaseOrderNos?: string[]; /** Single supplier code (drill when row has no supplier id); not used with `supplierIds`. */ supplierCode?: string; }; function appendPurchaseOrderListParams(p: URLSearchParams, filters?: PurchaseOrderChartFilters) { (filters?.supplierIds ?? []).forEach((id) => { if (Number.isFinite(id) && id > 0) p.append("supplierId", String(id)); }); (filters?.itemCodes ?? []).forEach((c) => { const t = String(c).trim(); if (t) p.append("itemCode", t); }); (filters?.purchaseOrderNos ?? []).forEach((n) => { const t = String(n).trim(); if (t) p.append("purchaseOrderNo", t); }); const sc = filters?.supplierCode?.trim(); if (sc) p.set("supplierCode", sc); } export interface PoFilterSupplierOption { supplierId: number; code: string; name: string; } export interface PoFilterItemOption { itemCode: string; itemName: string; } export interface PoFilterPoNoOption { poNo: string; } export interface PurchaseOrderFilterOptions { suppliers: PoFilterSupplierOption[]; items: PoFilterItemOption[]; poNos: PoFilterPoNoOption[]; } export interface PurchaseOrderEstimatedArrivalRow { bucket: string; count: number; } export interface PurchaseOrderDetailByStatusRow { purchaseOrderId: number; purchaseOrderNo: string; status: string; orderDate: string; estimatedArrivalDate: string; /** Shop / supplier FK; use for grouping when code is blank */ supplierId: number | null; supplierCode: string; supplierName: string; itemCount: number; totalQty: number; } export interface PurchaseOrderItemRow { purchaseOrderLineId: number; itemCode: string; itemName: string; orderedQty: number; uom: string; receivedQty: number; pendingQty: number; } export interface StockInOutByDateRow { date: string; inQty: number; outQty: number; } export interface TopDeliveryItemsRow { itemCode: string; itemName: string; totalQty: number; } export interface StockBalanceTrendRow { date: string; balance: number; } export interface ConsumptionTrendByMonthRow { month: string; outQty: number; } export interface StaffDeliveryPerformanceRow { date: string; staffName: string; orderCount: number; totalMinutes: number; } export interface StaffOption { staffNo: string; name: string; } export async function fetchStaffDeliveryPerformanceHandlers(): Promise { const res = await clientAuthFetch(`${BASE}/staff-delivery-performance-handlers`); if (!res.ok) throw new Error("Failed to fetch staff list"); const data = await res.json(); if (!Array.isArray(data)) return []; return (data as Record[]).map((r: Record) => ({ staffNo: String(r.staffNo ?? ""), name: String(r.name ?? ""), })); } // Job order export interface JobOrderByStatusRow { status: string; count: number; } export interface JobOrderCountByDateRow { date: string; orderCount: number; } export interface JobOrderCreatedCompletedRow { date: string; createdCount: number; completedCount: number; } export interface ProductionScheduleByDateRow { date: string; scheduledItemCount: number; totalEstProdCount: number; } export interface PlannedDailyOutputRow { itemCode: string; itemName: string; dailyQty: number; } export async function fetchJobOrderByStatus( targetDate?: string ): Promise { const q = targetDate ? buildParams({ targetDate }) : ""; const res = await clientAuthFetch( q ? `${BASE}/job-order-by-status?${q}` : `${BASE}/job-order-by-status` ); if (!res.ok) throw new Error("Failed to fetch job order by status"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ status: String(r.status ?? ""), count: Number(r.count ?? 0), })); } export async function fetchJobOrderCountByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/job-order-count-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch job order count by date"); const data = await res.json(); return normalizeChartRows(data, "date", ["orderCount"]); } export async function fetchJobOrderCreatedCompletedByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch( `${BASE}/job-order-created-completed-by-date?${q}` ); if (!res.ok) throw new Error("Failed to fetch job order created/completed"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ date: String(r.date ?? ""), createdCount: Number(r.createdCount ?? 0), completedCount: Number(r.completedCount ?? 0), })); } export interface JobMaterialPendingPickedRow { date: string; pendingCount: number; pickedCount: number; } export async function fetchJobMaterialPendingPickedByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/job-material-pending-picked-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch job material pending/picked"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ date: String(r.date ?? ""), pendingCount: Number(r.pendingCount ?? 0), pickedCount: Number(r.pickedCount ?? 0), })); } export interface JobProcessPendingCompletedRow { date: string; pendingCount: number; completedCount: number; } export async function fetchJobProcessPendingCompletedByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/job-process-pending-completed-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch job process pending/completed"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ date: String(r.date ?? ""), pendingCount: Number(r.pendingCount ?? 0), completedCount: Number(r.completedCount ?? 0), })); } export interface JobEquipmentWorkingWorkedRow { date: string; workingCount: number; workedCount: number; } export async function fetchJobEquipmentWorkingWorkedByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/job-equipment-working-worked-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch job equipment working/worked"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ date: String(r.date ?? ""), workingCount: Number(r.workingCount ?? 0), workedCount: Number(r.workedCount ?? 0), })); } export interface JobOrderBoardRow { jobOrderId: number; code: string; status: string; planStart: string; actualStart: string; planEnd: string; actualEnd: string; materialPendingCount: number; materialPickedCount: number; processTotalCount: number; processCompletedCount: number; currentProcessCode: string; currentProcessName: string; currentProcessStartTime: string; /** FG/WIP job stock-in: sum acceptedQty on all linked lines */ stockInAcceptedQtyTotal: number; /** Lines QC-passed, waiting putaway (receiving / received) */ fgReadyToStockInCount: number; fgReadyToStockInQty: number; fgInQcLineCount: number; fgInQcQty: number; fgStockedQty: number; /** Same sources as /jo/edit 工藝流程 summary (product process + lines) */ itemCode: string; itemName: string; jobTypeName: string; reqQty: number; outputQtyUom: string; productionDate: string; /** Sum of line processingTime (matches ProcessSummaryHeader 預計所需時間) */ planProcessingMinsTotal: number; /** Sum of setup + changeover minutes on all lines */ planSetupChangeoverMinsTotal: number; productProcessStart: string; /** Σ line durations in decimal minutes (seconds÷60); sub-minute shown; Pass w/o endTime uses planned processing min */ actualLineMinsTotal: number; } function numField(v: unknown): number { if (v == null || v === "") return 0; const n = Number(v); return Number.isFinite(n) ? n : 0; } function mapJobOrderBoardRow(r: Record): JobOrderBoardRow { const id = r.jobOrderId ?? r.joborderid; return { jobOrderId: Number(id ?? 0), code: String(r.code ?? ""), status: String(r.status ?? ""), planStart: String(r.planStart ?? r.planstart ?? ""), actualStart: String(r.actualStart ?? r.actualstart ?? ""), planEnd: String(r.planEnd ?? r.planend ?? ""), actualEnd: String(r.actualEnd ?? r.actualend ?? ""), materialPendingCount: Number(r.materialPendingCount ?? r.materialpendingcount ?? 0), materialPickedCount: Number(r.materialPickedCount ?? r.materialpickedcount ?? 0), processTotalCount: Number(r.processTotalCount ?? r.processtotalcount ?? 0), processCompletedCount: Number(r.processCompletedCount ?? r.processcompletedcount ?? 0), currentProcessCode: String(r.currentProcessCode ?? r.currentprocesscode ?? ""), currentProcessName: String(r.currentProcessName ?? r.currentprocessname ?? ""), currentProcessStartTime: String(r.currentProcessStartTime ?? r.currentprocessstarttime ?? ""), stockInAcceptedQtyTotal: Number(r.stockInAcceptedQtyTotal ?? r.stockinacceptedqtytotal ?? 0), fgReadyToStockInCount: Number(r.fgReadyToStockInCount ?? r.fgreadytostockincount ?? 0), fgReadyToStockInQty: Number(r.fgReadyToStockInQty ?? r.fgreadytostockinqty ?? 0), fgInQcLineCount: Number(r.fgInQcLineCount ?? r.fginqclinecount ?? 0), fgInQcQty: Number(r.fgInQcQty ?? r.fginqcqty ?? 0), fgStockedQty: Number(r.fgStockedQty ?? r.fgstockedqty ?? 0), itemCode: String(r.itemCode ?? r.itemcode ?? ""), itemName: String(r.itemName ?? r.itemname ?? ""), jobTypeName: String(r.jobTypeName ?? r.jobtypename ?? ""), reqQty: numField(r.reqQty ?? r.reqqty), outputQtyUom: String(r.outputQtyUom ?? r.outputqtyuom ?? ""), productionDate: String(r.productionDate ?? r.productiondate ?? ""), planProcessingMinsTotal: numField(r.planProcessingMinsTotal ?? r.planprocessingminstotal), planSetupChangeoverMinsTotal: numField(r.planSetupChangeoverMinsTotal ?? r.plansetupchangeoverminstotal), productProcessStart: String(r.productProcessStart ?? r.productprocessstart ?? ""), actualLineMinsTotal: numField(r.actualLineMinsTotal ?? r.actuallineminstotal), }; } /** Per-job board rows. With [incompleteOnly], excludes status completed (backend LOWER(status) <> 'completed'). */ export async function fetchJobOrderBoard( targetDate?: string, opts?: { incompleteOnly?: boolean }, ): Promise { const params: Record = {}; if (targetDate) params.targetDate = targetDate; if (opts?.incompleteOnly) params.incompleteOnly = "true"; const q = buildParams(params); const res = await clientAuthFetch(q ? `${BASE}/job-order-board?${q}` : `${BASE}/job-order-board`); if (!res.ok) throw new Error("Failed to fetch job order board"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map(mapJobOrderBoardRow); } export interface ProcessBoardRow { jopId: number; jobOrderId: number; jobOrderCode: string; jobOrderStatus: string; processId: number; processCode: string; processName: string; seqNo: number; rowStatus: string; jobPlanStart: string; startTime: string; endTime: string; /** Derived: pending | in_progress | completed */ boardStatus: string; /** 工藝流程步驟名稱(productprocessline.name;多筆以 | 分隔);無明細時為主檔工序名。 */ lineStepName: string; /** 描述 */ lineDescription: string; /** 設備類型-設備名稱-編號(與工單工藝流程一致) */ lineEquipmentLabel: string; /** 操作員/員工顯示名 */ lineOperatorInfo: string; itemCode: string; itemName: string; jobTypeName: string; reqQty: number; outputQtyUom: string; productionDate: string; planProcessingMinsTotal: number; planSetupChangeoverMinsTotal: number; productProcessStart: string; actualLineMinsTotal: number; /** This BOM step: sum(processing+setup+changeover) on matching lines */ stepPlanMins: number; /** This BOM step: Σ line durations in decimal minutes (seconds÷60); Pass/Completed without endTime uses planned processing min as fallback */ stepActualMins: number; } function mapProcessBoardRow(r: Record): ProcessBoardRow { return { jopId: Number(r.jopId ?? r.jopid ?? 0), jobOrderId: Number(r.jobOrderId ?? r.joborderid ?? 0), jobOrderCode: String(r.jobOrderCode ?? r.jobordercode ?? ""), jobOrderStatus: String(r.jobOrderStatus ?? r.joborderstatus ?? ""), processId: Number(r.processId ?? r.processid ?? 0), processCode: String(r.processCode ?? r.processcode ?? ""), processName: String(r.processName ?? r.processname ?? ""), seqNo: Number(r.seqNo ?? r.seqno ?? 0), rowStatus: String(r.rowStatus ?? r.rowstatus ?? ""), jobPlanStart: String(r.jobPlanStart ?? r.jobplanstart ?? ""), startTime: String(r.startTime ?? r.starttime ?? ""), endTime: String(r.endTime ?? r.endtime ?? ""), boardStatus: String(r.boardStatus ?? r.boardstatus ?? "pending").toLowerCase(), lineStepName: String(r.lineStepName ?? r.linestepname ?? r.line_step_name ?? ""), lineDescription: String(r.lineDescription ?? r.linedescription ?? r.line_description ?? ""), lineEquipmentLabel: String(r.lineEquipmentLabel ?? r.lineequipmentlabel ?? r.line_equipment_label ?? ""), lineOperatorInfo: String(r.lineOperatorInfo ?? r.lineoperatorinfo ?? r.line_operator_info ?? ""), itemCode: String(r.itemCode ?? r.itemcode ?? ""), itemName: String(r.itemName ?? r.itemname ?? ""), jobTypeName: String(r.jobTypeName ?? r.jobtypename ?? ""), reqQty: numField(r.reqQty ?? r.reqqty), outputQtyUom: String(r.outputQtyUom ?? r.outputqtyuom ?? ""), productionDate: String(r.productionDate ?? r.productiondate ?? ""), planProcessingMinsTotal: numField(r.planProcessingMinsTotal ?? r.planprocessingminstotal), planSetupChangeoverMinsTotal: numField(r.planSetupChangeoverMinsTotal ?? r.plansetupchangeoverminstotal), productProcessStart: String(r.productProcessStart ?? r.productprocessstart ?? ""), actualLineMinsTotal: numField(r.actualLineMinsTotal ?? r.actuallineminstotal), stepPlanMins: numField(r.stepPlanMins ?? r.stepplanmins), stepActualMins: numField(r.stepActualMins ?? r.stepactualmins), }; } /** Per job_order_process line; same filters as job-order board. */ export async function fetchProcessBoard( targetDate?: string, opts?: { incompleteOnly?: boolean }, ): Promise { const params: Record = {}; if (targetDate) params.targetDate = targetDate; if (opts?.incompleteOnly) params.incompleteOnly = "true"; const q = buildParams(params); const res = await clientAuthFetch(q ? `${BASE}/process-board?${q}` : `${BASE}/process-board`); if (!res.ok) throw new Error("Failed to fetch process board"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map(mapProcessBoardRow); } export interface EquipmentUsageBoardRow { jopdId: number; equipmentId: number; equipmentCode: string; equipmentName: string; jobOrderId: number; jobOrderCode: string; jobPlanStart: string; processCode: string; processName: string; operatingStart: string; operatingEnd: string; /** Estimated usage minutes (start–end diff, or 產線 processingTime when Pass/Completed without end). */ usageMinutes: number; workingNow: number; operatorUsername: string; operatorName: string; } function mapEquipmentUsageBoardRow(r: Record): EquipmentUsageBoardRow { return { jopdId: Number(r.jopdId ?? r.jopdid ?? 0), equipmentId: Number(r.equipmentId ?? r.equipmentid ?? 0), equipmentCode: String(r.equipmentCode ?? r.equipmentcode ?? ""), equipmentName: String(r.equipmentName ?? r.equipmentname ?? ""), jobOrderId: Number(r.jobOrderId ?? r.joborderid ?? 0), jobOrderCode: String(r.jobOrderCode ?? r.jobordercode ?? ""), jobPlanStart: String(r.jobPlanStart ?? r.jobplanstart ?? ""), processCode: String(r.processCode ?? r.processcode ?? ""), processName: String(r.processName ?? r.processname ?? ""), operatingStart: String(r.operatingStart ?? r.operatingstart ?? ""), operatingEnd: String(r.operatingEnd ?? r.operatingend ?? ""), usageMinutes: Number(r.usageMinutes ?? r.usageminutes ?? 0), workingNow: Number(r.workingNow ?? r.workingnow ?? 0), operatorUsername: String(r.operatorUsername ?? r.operatorusername ?? ""), operatorName: String(r.operatorName ?? r.operatorname ?? ""), }; } /** Day = COALESCE(line/jopd times, jop.endTime, planStart). Includes productprocessline (工藝流程) and job_order_process_detail. Omit targetDate = server today. */ export async function fetchEquipmentUsageBoard(targetDate?: string): Promise { const q = buildParams({ targetDate: targetDate ?? "" }); const res = await clientAuthFetch(q ? `${BASE}/equipment-usage-board?${q}` : `${BASE}/equipment-usage-board`); if (!res.ok) throw new Error("Failed to fetch equipment usage board"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map(mapEquipmentUsageBoardRow); } export async function fetchProductionScheduleByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch( `${BASE}/production-schedule-by-date?${q}` ); if (!res.ok) throw new Error("Failed to fetch production schedule by date"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ date: String(r.date ?? ""), scheduledItemCount: Number(r.scheduledItemCount ?? r.scheduleCount ?? 0), totalEstProdCount: Number(r.totalEstProdCount ?? 0), })); } export async function fetchPlannedDailyOutputByItem( limit = 20 ): Promise { const res = await clientAuthFetch( `${BASE}/planned-daily-output-by-item?limit=${limit}` ); if (!res.ok) throw new Error("Failed to fetch planned daily output"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ itemCode: String(r.itemCode ?? ""), itemName: String(r.itemName ?? ""), dailyQty: Number(r.dailyQty ?? 0), })); } /** Planned production by date and by item (production_schedule). */ export interface PlannedOutputByDateAndItemRow { date: string; itemCode: string; itemName: string; qty: number; } export async function fetchPlannedOutputByDateAndItem( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch( q ? `${BASE}/planned-output-by-date-and-item?${q}` : `${BASE}/planned-output-by-date-and-item` ); if (!res.ok) throw new Error("Failed to fetch planned output by date and item"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ date: String(r.date ?? ""), itemCode: String(r.itemCode ?? ""), itemName: String(r.itemName ?? ""), qty: Number(r.qty ?? 0), })); } export async function fetchStaffDeliveryPerformance( startDate?: string, endDate?: string, staffNos?: string[] ): Promise { const p = new URLSearchParams(); if (startDate) p.set("startDate", startDate); if (endDate) p.set("endDate", endDate); (staffNos ?? []).forEach((no) => p.append("staffNo", no)); const q = p.toString(); const res = await clientAuthFetch( q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance` ); if (!res.ok) throw new Error("Failed to fetch staff delivery performance"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => { // Accept camelCase or lowercase keys (JDBC/DB may return different casing) const row = r as Record; return { date: String(row.date ?? row.Date ?? ""), staffName: String(row.staffName ?? row.staffname ?? ""), orderCount: Number(row.orderCount ?? row.ordercount ?? 0), totalMinutes: Number(row.totalMinutes ?? row.totalminutes ?? 0), }; }); } export async function fetchStockTransactionsByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/stock-transactions-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch stock transactions by date"); const data = await res.json(); return normalizeChartRows(data, "date", ["inQty", "outQty", "totalQty"]); } export async function fetchDeliveryOrderByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/delivery-order-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch delivery order by date"); const data = await res.json(); return normalizeChartRows(data, "date", ["orderCount", "totalQty"]); } export async function fetchPurchaseOrderByStatus( targetDate?: string, filters?: PurchaseOrderChartFilters ): Promise { const p = new URLSearchParams(); if (targetDate) p.set("targetDate", targetDate); appendPurchaseOrderListParams(p, filters); const q = p.toString(); const res = await clientAuthFetch( q ? `${BASE}/purchase-order-by-status?${q}` : `${BASE}/purchase-order-by-status` ); if (!res.ok) throw new Error("Failed to fetch purchase order by status"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ status: String(r.status ?? ""), count: Number(r.count ?? 0), })); } export async function fetchPurchaseOrderFilterOptions( targetDate?: string ): Promise { const p = new URLSearchParams(); if (targetDate) p.set("targetDate", targetDate); const q = p.toString(); const res = await clientAuthFetch( q ? `${BASE}/purchase-order-filter-options?${q}` : `${BASE}/purchase-order-filter-options` ); if (!res.ok) throw new Error("Failed to fetch purchase order filter options"); const data = await res.json(); const row = (data ?? {}) as Record; const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record[]; const items = (Array.isArray(row.items) ? row.items : []) as Record[]; const poNos = (Array.isArray(row.poNos) ? row.poNos : []) as Record[]; return { suppliers: suppliers.map((r) => ({ supplierId: Number(r.supplierId ?? r.supplierid ?? 0), code: String(r.code ?? ""), name: String(r.name ?? ""), })), items: items.map((r) => ({ itemCode: String(r.itemCode ?? r.itemcode ?? ""), itemName: String(r.itemName ?? r.itemname ?? ""), })), poNos: poNos.map((r) => ({ poNo: String(r.poNo ?? r.pono ?? ""), })), }; } export async function fetchPurchaseOrderEstimatedArrivalSummary( targetDate?: string, filters?: PurchaseOrderChartFilters ): Promise { const p = new URLSearchParams(); if (targetDate) p.set("targetDate", targetDate); appendPurchaseOrderListParams(p, filters); const q = p.toString(); const res = await clientAuthFetch( q ? `${BASE}/purchase-order-estimated-arrival-summary?${q}` : `${BASE}/purchase-order-estimated-arrival-summary` ); if (!res.ok) throw new Error("Failed to fetch estimated arrival summary"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ bucket: String(r.bucket ?? ""), count: Number(r.count ?? 0), })); } export interface EstimatedArrivalBreakdownSupplierRow { supplierId: number | null; supplierCode: string; supplierName: string; poCount: number; } export interface EstimatedArrivalBreakdownItemRow { itemCode: string; itemName: string; poCount: number; totalQty: number; } export interface EstimatedArrivalBreakdownPoRow { purchaseOrderId: number; purchaseOrderNo: string; status: string; orderDate: string; supplierId: number | null; supplierCode: string; supplierName: string; } export interface PurchaseOrderEstimatedArrivalBreakdown { suppliers: EstimatedArrivalBreakdownSupplierRow[]; items: EstimatedArrivalBreakdownItemRow[]; purchaseOrders: EstimatedArrivalBreakdownPoRow[]; } /** Related suppliers / items / POs for one 預計送貨 bucket (same bar filters as the donut). */ export async function fetchPurchaseOrderEstimatedArrivalBreakdown( targetDate: string, estimatedArrivalBucket: string, filters?: PurchaseOrderChartFilters ): Promise { const p = new URLSearchParams(); p.set("targetDate", targetDate); p.set("estimatedArrivalBucket", estimatedArrivalBucket.trim().toLowerCase()); appendPurchaseOrderListParams(p, filters); const res = await clientAuthFetch(`${BASE}/purchase-order-estimated-arrival-breakdown?${p.toString()}`); if (!res.ok) throw new Error("Failed to fetch estimated arrival breakdown"); const data = await res.json(); const row = (data ?? {}) as Record; const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record[]; const items = (Array.isArray(row.items) ? row.items : []) as Record[]; const purchaseOrders = (Array.isArray(row.purchaseOrders) ? row.purchaseOrders : []) as Record[]; return { suppliers: suppliers.map((r) => ({ supplierId: (() => { const v = r.supplierId ?? r.supplierid; if (v == null || v === "") return null; const n = Number(v); return Number.isFinite(n) ? n : null; })(), supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""), supplierName: String(r.supplierName ?? r.suppliername ?? ""), poCount: Number(r.poCount ?? r.pocount ?? 0), })), items: items.map((r) => ({ itemCode: String(r.itemCode ?? r.itemcode ?? ""), itemName: String(r.itemName ?? r.itemname ?? ""), poCount: Number(r.poCount ?? r.pocount ?? 0), totalQty: Number(r.totalQty ?? r.totalqty ?? 0), })), purchaseOrders: purchaseOrders.map((r) => ({ purchaseOrderId: Number(r.purchaseOrderId ?? r.purchaseorderid ?? 0), purchaseOrderNo: String(r.purchaseOrderNo ?? r.purchaseorderno ?? ""), status: String(r.status ?? ""), orderDate: String(r.orderDate ?? r.orderdate ?? ""), supplierId: (() => { const v = r.supplierId ?? r.supplierid; if (v == null || v === "") return null; const n = Number(v); return Number.isFinite(n) ? n : null; })(), supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""), supplierName: String(r.supplierName ?? r.suppliername ?? ""), })), }; } export type PurchaseOrderDrillQuery = PurchaseOrderChartFilters & { /** order = PO order date; complete = PO complete date (for received/completed on a day) */ dateFilter?: "order" | "complete"; /** delivered | not_delivered | cancelled | other — same as 預計送貨 donut buckets */ estimatedArrivalBucket?: string; }; export async function fetchPurchaseOrderDetailsByStatus( status: string, targetDate?: string, opts?: PurchaseOrderDrillQuery ): Promise { const p = new URLSearchParams(); p.set("status", status.trim().toLowerCase()); if (targetDate) p.set("targetDate", targetDate); if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter); if (opts?.estimatedArrivalBucket?.trim()) { p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase()); } appendPurchaseOrderListParams(p, opts); const q = p.toString(); const res = await clientAuthFetch(`${BASE}/purchase-order-details-by-status?${q}`); if (!res.ok) throw new Error("Failed to fetch purchase order details by status"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ purchaseOrderId: Number(r.purchaseOrderId ?? 0), purchaseOrderNo: String(r.purchaseOrderNo ?? ""), status: String(r.status ?? ""), orderDate: String(r.orderDate ?? ""), estimatedArrivalDate: String(r.estimatedArrivalDate ?? ""), supplierId: (() => { const v = r.supplierId; if (v == null || v === "") return null; const n = Number(v); return Number.isFinite(n) && n > 0 ? n : null; })(), supplierCode: String(r.supplierCode ?? ""), supplierName: String(r.supplierName ?? ""), itemCount: Number(r.itemCount ?? 0), totalQty: Number(r.totalQty ?? 0), })); } export async function fetchPurchaseOrderItems( purchaseOrderId: number ): Promise { const q = buildParams({ purchaseOrderId }); const res = await clientAuthFetch(`${BASE}/purchase-order-items?${q}`); if (!res.ok) throw new Error("Failed to fetch purchase order items"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ purchaseOrderLineId: Number(r.purchaseOrderLineId ?? 0), itemCode: String(r.itemCode ?? ""), itemName: String(r.itemName ?? ""), orderedQty: Number(r.orderedQty ?? 0), uom: String(r.uom ?? ""), receivedQty: Number(r.receivedQty ?? 0), pendingQty: Number(r.pendingQty ?? 0), })); } export async function fetchPurchaseOrderItemsByStatus( status: string, targetDate?: string, opts?: PurchaseOrderDrillQuery ): Promise { const p = new URLSearchParams(); p.set("status", status.trim().toLowerCase()); if (targetDate) p.set("targetDate", targetDate); if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter); if (opts?.estimatedArrivalBucket?.trim()) { p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase()); } appendPurchaseOrderListParams(p, opts); const q = p.toString(); const res = await clientAuthFetch(`${BASE}/purchase-order-items-by-status?${q}`); if (!res.ok) throw new Error("Failed to fetch purchase order items by status"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ purchaseOrderLineId: 0, itemCode: String(r.itemCode ?? ""), itemName: String(r.itemName ?? ""), orderedQty: Number(r.orderedQty ?? 0), uom: String(r.uom ?? ""), receivedQty: Number(r.receivedQty ?? 0), pendingQty: Number(r.pendingQty ?? 0), })); } export async function fetchStockInOutByDate( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch(`${BASE}/stock-in-out-by-date?${q}`); if (!res.ok) throw new Error("Failed to fetch stock in/out by date"); const data = await res.json(); return normalizeChartRows(data, "date", ["inQty", "outQty"]); } export interface TopDeliveryItemOption { itemCode: string; itemName: string; } export async function fetchTopDeliveryItemsItemOptions( startDate?: string, endDate?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" }); const res = await clientAuthFetch( q ? `${BASE}/top-delivery-items-item-options?${q}` : `${BASE}/top-delivery-items-item-options` ); if (!res.ok) throw new Error("Failed to fetch item options"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ itemCode: String(r.itemCode ?? ""), itemName: String(r.itemName ?? ""), })); } export async function fetchTopDeliveryItems( startDate?: string, endDate?: string, limit = 10, itemCodes?: string[] ): Promise { const p = new URLSearchParams(); if (startDate) p.set("startDate", startDate); if (endDate) p.set("endDate", endDate); p.set("limit", String(limit)); (itemCodes ?? []).forEach((code) => p.append("itemCode", code)); const q = p.toString(); const res = await clientAuthFetch(`${BASE}/top-delivery-items?${q}`); if (!res.ok) throw new Error("Failed to fetch top delivery items"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ itemCode: String(r.itemCode ?? ""), itemName: String(r.itemName ?? ""), totalQty: Number(r.totalQty ?? 0), })); } export async function fetchStockBalanceTrend( startDate?: string, endDate?: string, itemCode?: string ): Promise { const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "", itemCode: itemCode ?? "", }); const res = await clientAuthFetch(`${BASE}/stock-balance-trend?${q}`); if (!res.ok) throw new Error("Failed to fetch stock balance trend"); const data = await res.json(); return normalizeChartRows(data, "date", ["balance"]); } export async function fetchConsumptionTrendByMonth( year?: number, startDate?: string, endDate?: string, itemCode?: string ): Promise { const q = buildParams({ year: year ?? "", startDate: startDate ?? "", endDate: endDate ?? "", itemCode: itemCode ?? "", }); const res = await clientAuthFetch(`${BASE}/consumption-trend-by-month?${q}`); if (!res.ok) throw new Error("Failed to fetch consumption trend"); const data = await res.json(); return ((Array.isArray(data) ? data : []) as Record[]).map((r: Record) => ({ month: String(r.month ?? ""), outQty: Number(r.outQty ?? 0), })); } /** Normalize rows: ensure date key is string and numeric keys are numbers (backend may return BigDecimal/Long). */ function normalizeChartRows( rows: unknown[], dateKey: string, numberKeys: string[] ): T[] { if (!Array.isArray(rows)) return []; return rows.map((r: unknown) => { const row = r as Record; const out: Record = {}; out[dateKey] = row[dateKey] != null ? String(row[dateKey]) : ""; numberKeys.forEach((k) => { out[k] = Number(row[k]) || 0; }); return out as T; }); }