|
|
|
@@ -0,0 +1,975 @@ |
|
|
|
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<string, string | number | undefined>) { |
|
|
|
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<StaffOption[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<JobOrderByStatusRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
status: String(r.status ?? ""), |
|
|
|
count: Number(r.count ?? 0), |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
export async function fetchJobOrderCountByDate( |
|
|
|
startDate?: string, |
|
|
|
endDate?: string |
|
|
|
): Promise<JobOrderCountByDateRow[]> { |
|
|
|
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<JobOrderCreatedCompletedRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<JobMaterialPendingPickedRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<JobProcessPendingCompletedRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<JobEquipmentWorkingWorkedRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<string, unknown>): 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<JobOrderBoardRow[]> { |
|
|
|
const params: Record<string, string | number | undefined> = {}; |
|
|
|
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<string, unknown>[]).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<string, unknown>): 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<ProcessBoardRow[]> { |
|
|
|
const params: Record<string, string | number | undefined> = {}; |
|
|
|
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<string, unknown>[]).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<string, unknown>): 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<EquipmentUsageBoardRow[]> { |
|
|
|
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<string, unknown>[]).map(mapEquipmentUsageBoardRow); |
|
|
|
} |
|
|
|
|
|
|
|
export async function fetchProductionScheduleByDate( |
|
|
|
startDate?: string, |
|
|
|
endDate?: string |
|
|
|
): Promise<ProductionScheduleByDateRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
date: String(r.date ?? ""), |
|
|
|
scheduledItemCount: Number(r.scheduledItemCount ?? r.scheduleCount ?? 0), |
|
|
|
totalEstProdCount: Number(r.totalEstProdCount ?? 0), |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
export async function fetchPlannedDailyOutputByItem( |
|
|
|
limit = 20 |
|
|
|
): Promise<PlannedDailyOutputRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<PlannedOutputByDateAndItemRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<StaffDeliveryPerformanceRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => { |
|
|
|
// Accept camelCase or lowercase keys (JDBC/DB may return different casing) |
|
|
|
const row = r as Record<string, unknown>; |
|
|
|
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<StockTransactionsByDateRow[]> { |
|
|
|
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<DeliveryOrderByDateRow[]> { |
|
|
|
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<PurchaseOrderByStatusRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
status: String(r.status ?? ""), |
|
|
|
count: Number(r.count ?? 0), |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
export async function fetchPurchaseOrderFilterOptions( |
|
|
|
targetDate?: string |
|
|
|
): Promise<PurchaseOrderFilterOptions> { |
|
|
|
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<string, unknown>; |
|
|
|
const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[]; |
|
|
|
const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[]; |
|
|
|
const poNos = (Array.isArray(row.poNos) ? row.poNos : []) as Record<string, unknown>[]; |
|
|
|
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<PurchaseOrderEstimatedArrivalRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<PurchaseOrderEstimatedArrivalBreakdown> { |
|
|
|
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<string, unknown>; |
|
|
|
const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[]; |
|
|
|
const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[]; |
|
|
|
const purchaseOrders = (Array.isArray(row.purchaseOrders) ? row.purchaseOrders : []) as Record<string, unknown>[]; |
|
|
|
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<PurchaseOrderDetailByStatusRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<PurchaseOrderItemRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<PurchaseOrderItemRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<StockInOutByDateRow[]> { |
|
|
|
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<TopDeliveryItemOption[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
itemCode: String(r.itemCode ?? ""), |
|
|
|
itemName: String(r.itemName ?? ""), |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
export async function fetchTopDeliveryItems( |
|
|
|
startDate?: string, |
|
|
|
endDate?: string, |
|
|
|
limit = 10, |
|
|
|
itemCodes?: string[] |
|
|
|
): Promise<TopDeliveryItemsRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
itemCode: String(r.itemCode ?? ""), |
|
|
|
itemName: String(r.itemName ?? ""), |
|
|
|
totalQty: Number(r.totalQty ?? 0), |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
export async function fetchStockBalanceTrend( |
|
|
|
startDate?: string, |
|
|
|
endDate?: string, |
|
|
|
itemCode?: string |
|
|
|
): Promise<StockBalanceTrendRow[]> { |
|
|
|
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<ConsumptionTrendByMonthRow[]> { |
|
|
|
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<string, unknown>[]).map((r: Record<string, unknown>) => ({ |
|
|
|
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<T>( |
|
|
|
rows: unknown[], |
|
|
|
dateKey: string, |
|
|
|
numberKeys: string[] |
|
|
|
): T[] { |
|
|
|
if (!Array.isArray(rows)) return []; |
|
|
|
return rows.map((r: unknown) => { |
|
|
|
const row = r as Record<string, unknown>; |
|
|
|
const out: Record<string, unknown> = {}; |
|
|
|
out[dateKey] = row[dateKey] != null ? String(row[dateKey]) : ""; |
|
|
|
numberKeys.forEach((k) => { |
|
|
|
out[k] = Number(row[k]) || 0; |
|
|
|
}); |
|
|
|
return out as T; |
|
|
|
}); |
|
|
|
} |