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 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; }); }