|
|
@@ -29,6 +29,81 @@ export interface PurchaseOrderByStatusRow { |
|
|
count: number; |
|
|
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 { |
|
|
export interface StockInOutByDateRow { |
|
|
date: string; |
|
|
date: string; |
|
|
inQty: number; |
|
|
inQty: number; |
|
|
@@ -317,11 +392,13 @@ export async function fetchDeliveryOrderByDate( |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export async function fetchPurchaseOrderByStatus( |
|
|
export async function fetchPurchaseOrderByStatus( |
|
|
targetDate?: string |
|
|
|
|
|
|
|
|
targetDate?: string, |
|
|
|
|
|
filters?: PurchaseOrderChartFilters |
|
|
): Promise<PurchaseOrderByStatusRow[]> { |
|
|
): Promise<PurchaseOrderByStatusRow[]> { |
|
|
const q = targetDate |
|
|
|
|
|
? buildParams({ targetDate }) |
|
|
|
|
|
: ""; |
|
|
|
|
|
|
|
|
const p = new URLSearchParams(); |
|
|
|
|
|
if (targetDate) p.set("targetDate", targetDate); |
|
|
|
|
|
appendPurchaseOrderListParams(p, filters); |
|
|
|
|
|
const q = p.toString(); |
|
|
const res = await clientAuthFetch( |
|
|
const res = await clientAuthFetch( |
|
|
q ? `${BASE}/purchase-order-by-status?${q}` : `${BASE}/purchase-order-by-status` |
|
|
q ? `${BASE}/purchase-order-by-status?${q}` : `${BASE}/purchase-order-by-status` |
|
|
); |
|
|
); |
|
|
@@ -333,6 +410,229 @@ export async function fetchPurchaseOrderByStatus( |
|
|
})); |
|
|
})); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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( |
|
|
export async function fetchStockInOutByDate( |
|
|
startDate?: string, |
|
|
startDate?: string, |
|
|
endDate?: string |
|
|
endDate?: string |
|
|
|