FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

744 lines
26 KiB

  1. import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
  2. import { NEXT_PUBLIC_API_URL } from "@/config/api";
  3. const BASE = `${NEXT_PUBLIC_API_URL}/chart`;
  4. function buildParams(params: Record<string, string | number | undefined>) {
  5. const p = new URLSearchParams();
  6. Object.entries(params).forEach(([k, v]) => {
  7. if (v !== undefined && v !== "") p.set(k, String(v));
  8. });
  9. return p.toString();
  10. }
  11. export interface StockTransactionsByDateRow {
  12. date: string;
  13. inQty: number;
  14. outQty: number;
  15. totalQty: number;
  16. }
  17. export interface DeliveryOrderByDateRow {
  18. date: string;
  19. orderCount: number;
  20. totalQty: number;
  21. }
  22. export interface PurchaseOrderByStatusRow {
  23. status: string;
  24. count: number;
  25. }
  26. /** Multi-select filters for purchase charts (repeated `supplierId` / `itemCode` / `purchaseOrderNo` query params). */
  27. export type PurchaseOrderChartFilters = {
  28. supplierIds?: number[];
  29. itemCodes?: string[];
  30. purchaseOrderNos?: string[];
  31. /** Single supplier code (drill when row has no supplier id); not used with `supplierIds`. */
  32. supplierCode?: string;
  33. };
  34. function appendPurchaseOrderListParams(p: URLSearchParams, filters?: PurchaseOrderChartFilters) {
  35. (filters?.supplierIds ?? []).forEach((id) => {
  36. if (Number.isFinite(id) && id > 0) p.append("supplierId", String(id));
  37. });
  38. (filters?.itemCodes ?? []).forEach((c) => {
  39. const t = String(c).trim();
  40. if (t) p.append("itemCode", t);
  41. });
  42. (filters?.purchaseOrderNos ?? []).forEach((n) => {
  43. const t = String(n).trim();
  44. if (t) p.append("purchaseOrderNo", t);
  45. });
  46. const sc = filters?.supplierCode?.trim();
  47. if (sc) p.set("supplierCode", sc);
  48. }
  49. export interface PoFilterSupplierOption {
  50. supplierId: number;
  51. code: string;
  52. name: string;
  53. }
  54. export interface PoFilterItemOption {
  55. itemCode: string;
  56. itemName: string;
  57. }
  58. export interface PoFilterPoNoOption {
  59. poNo: string;
  60. }
  61. export interface PurchaseOrderFilterOptions {
  62. suppliers: PoFilterSupplierOption[];
  63. items: PoFilterItemOption[];
  64. poNos: PoFilterPoNoOption[];
  65. }
  66. export interface PurchaseOrderEstimatedArrivalRow {
  67. bucket: string;
  68. count: number;
  69. }
  70. export interface PurchaseOrderDetailByStatusRow {
  71. purchaseOrderId: number;
  72. purchaseOrderNo: string;
  73. status: string;
  74. orderDate: string;
  75. estimatedArrivalDate: string;
  76. /** Shop / supplier FK; use for grouping when code is blank */
  77. supplierId: number | null;
  78. supplierCode: string;
  79. supplierName: string;
  80. itemCount: number;
  81. totalQty: number;
  82. }
  83. export interface PurchaseOrderItemRow {
  84. purchaseOrderLineId: number;
  85. itemCode: string;
  86. itemName: string;
  87. orderedQty: number;
  88. uom: string;
  89. receivedQty: number;
  90. pendingQty: number;
  91. }
  92. export interface StockInOutByDateRow {
  93. date: string;
  94. inQty: number;
  95. outQty: number;
  96. }
  97. export interface TopDeliveryItemsRow {
  98. itemCode: string;
  99. itemName: string;
  100. totalQty: number;
  101. }
  102. export interface StockBalanceTrendRow {
  103. date: string;
  104. balance: number;
  105. }
  106. export interface ConsumptionTrendByMonthRow {
  107. month: string;
  108. outQty: number;
  109. }
  110. export interface StaffDeliveryPerformanceRow {
  111. date: string;
  112. staffName: string;
  113. orderCount: number;
  114. totalMinutes: number;
  115. }
  116. export interface StaffOption {
  117. staffNo: string;
  118. name: string;
  119. }
  120. export async function fetchStaffDeliveryPerformanceHandlers(): Promise<StaffOption[]> {
  121. const res = await clientAuthFetch(`${BASE}/staff-delivery-performance-handlers`);
  122. if (!res.ok) throw new Error("Failed to fetch staff list");
  123. const data = await res.json();
  124. if (!Array.isArray(data)) return [];
  125. return (data as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  126. staffNo: String(r.staffNo ?? ""),
  127. name: String(r.name ?? ""),
  128. }));
  129. }
  130. // Job order
  131. export interface JobOrderByStatusRow {
  132. status: string;
  133. count: number;
  134. }
  135. export interface JobOrderCountByDateRow {
  136. date: string;
  137. orderCount: number;
  138. }
  139. export interface JobOrderCreatedCompletedRow {
  140. date: string;
  141. createdCount: number;
  142. completedCount: number;
  143. }
  144. export interface ProductionScheduleByDateRow {
  145. date: string;
  146. scheduledItemCount: number;
  147. totalEstProdCount: number;
  148. }
  149. export interface PlannedDailyOutputRow {
  150. itemCode: string;
  151. itemName: string;
  152. dailyQty: number;
  153. }
  154. export async function fetchJobOrderByStatus(
  155. targetDate?: string
  156. ): Promise<JobOrderByStatusRow[]> {
  157. const q = targetDate ? buildParams({ targetDate }) : "";
  158. const res = await clientAuthFetch(
  159. q ? `${BASE}/job-order-by-status?${q}` : `${BASE}/job-order-by-status`
  160. );
  161. if (!res.ok) throw new Error("Failed to fetch job order by status");
  162. const data = await res.json();
  163. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  164. status: String(r.status ?? ""),
  165. count: Number(r.count ?? 0),
  166. }));
  167. }
  168. export async function fetchJobOrderCountByDate(
  169. startDate?: string,
  170. endDate?: string
  171. ): Promise<JobOrderCountByDateRow[]> {
  172. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  173. const res = await clientAuthFetch(`${BASE}/job-order-count-by-date?${q}`);
  174. if (!res.ok) throw new Error("Failed to fetch job order count by date");
  175. const data = await res.json();
  176. return normalizeChartRows(data, "date", ["orderCount"]);
  177. }
  178. export async function fetchJobOrderCreatedCompletedByDate(
  179. startDate?: string,
  180. endDate?: string
  181. ): Promise<JobOrderCreatedCompletedRow[]> {
  182. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  183. const res = await clientAuthFetch(
  184. `${BASE}/job-order-created-completed-by-date?${q}`
  185. );
  186. if (!res.ok) throw new Error("Failed to fetch job order created/completed");
  187. const data = await res.json();
  188. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  189. date: String(r.date ?? ""),
  190. createdCount: Number(r.createdCount ?? 0),
  191. completedCount: Number(r.completedCount ?? 0),
  192. }));
  193. }
  194. export interface JobMaterialPendingPickedRow {
  195. date: string;
  196. pendingCount: number;
  197. pickedCount: number;
  198. }
  199. export async function fetchJobMaterialPendingPickedByDate(
  200. startDate?: string,
  201. endDate?: string
  202. ): Promise<JobMaterialPendingPickedRow[]> {
  203. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  204. const res = await clientAuthFetch(`${BASE}/job-material-pending-picked-by-date?${q}`);
  205. if (!res.ok) throw new Error("Failed to fetch job material pending/picked");
  206. const data = await res.json();
  207. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  208. date: String(r.date ?? ""),
  209. pendingCount: Number(r.pendingCount ?? 0),
  210. pickedCount: Number(r.pickedCount ?? 0),
  211. }));
  212. }
  213. export interface JobProcessPendingCompletedRow {
  214. date: string;
  215. pendingCount: number;
  216. completedCount: number;
  217. }
  218. export async function fetchJobProcessPendingCompletedByDate(
  219. startDate?: string,
  220. endDate?: string
  221. ): Promise<JobProcessPendingCompletedRow[]> {
  222. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  223. const res = await clientAuthFetch(`${BASE}/job-process-pending-completed-by-date?${q}`);
  224. if (!res.ok) throw new Error("Failed to fetch job process pending/completed");
  225. const data = await res.json();
  226. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  227. date: String(r.date ?? ""),
  228. pendingCount: Number(r.pendingCount ?? 0),
  229. completedCount: Number(r.completedCount ?? 0),
  230. }));
  231. }
  232. export interface JobEquipmentWorkingWorkedRow {
  233. date: string;
  234. workingCount: number;
  235. workedCount: number;
  236. }
  237. export async function fetchJobEquipmentWorkingWorkedByDate(
  238. startDate?: string,
  239. endDate?: string
  240. ): Promise<JobEquipmentWorkingWorkedRow[]> {
  241. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  242. const res = await clientAuthFetch(`${BASE}/job-equipment-working-worked-by-date?${q}`);
  243. if (!res.ok) throw new Error("Failed to fetch job equipment working/worked");
  244. const data = await res.json();
  245. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  246. date: String(r.date ?? ""),
  247. workingCount: Number(r.workingCount ?? 0),
  248. workedCount: Number(r.workedCount ?? 0),
  249. }));
  250. }
  251. export async function fetchProductionScheduleByDate(
  252. startDate?: string,
  253. endDate?: string
  254. ): Promise<ProductionScheduleByDateRow[]> {
  255. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  256. const res = await clientAuthFetch(
  257. `${BASE}/production-schedule-by-date?${q}`
  258. );
  259. if (!res.ok) throw new Error("Failed to fetch production schedule by date");
  260. const data = await res.json();
  261. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  262. date: String(r.date ?? ""),
  263. scheduledItemCount: Number(r.scheduledItemCount ?? r.scheduleCount ?? 0),
  264. totalEstProdCount: Number(r.totalEstProdCount ?? 0),
  265. }));
  266. }
  267. export async function fetchPlannedDailyOutputByItem(
  268. limit = 20
  269. ): Promise<PlannedDailyOutputRow[]> {
  270. const res = await clientAuthFetch(
  271. `${BASE}/planned-daily-output-by-item?limit=${limit}`
  272. );
  273. if (!res.ok) throw new Error("Failed to fetch planned daily output");
  274. const data = await res.json();
  275. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  276. itemCode: String(r.itemCode ?? ""),
  277. itemName: String(r.itemName ?? ""),
  278. dailyQty: Number(r.dailyQty ?? 0),
  279. }));
  280. }
  281. /** Planned production by date and by item (production_schedule). */
  282. export interface PlannedOutputByDateAndItemRow {
  283. date: string;
  284. itemCode: string;
  285. itemName: string;
  286. qty: number;
  287. }
  288. export async function fetchPlannedOutputByDateAndItem(
  289. startDate?: string,
  290. endDate?: string
  291. ): Promise<PlannedOutputByDateAndItemRow[]> {
  292. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  293. const res = await clientAuthFetch(
  294. q ? `${BASE}/planned-output-by-date-and-item?${q}` : `${BASE}/planned-output-by-date-and-item`
  295. );
  296. if (!res.ok) throw new Error("Failed to fetch planned output by date and item");
  297. const data = await res.json();
  298. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  299. date: String(r.date ?? ""),
  300. itemCode: String(r.itemCode ?? ""),
  301. itemName: String(r.itemName ?? ""),
  302. qty: Number(r.qty ?? 0),
  303. }));
  304. }
  305. export async function fetchStaffDeliveryPerformance(
  306. startDate?: string,
  307. endDate?: string,
  308. staffNos?: string[]
  309. ): Promise<StaffDeliveryPerformanceRow[]> {
  310. const p = new URLSearchParams();
  311. if (startDate) p.set("startDate", startDate);
  312. if (endDate) p.set("endDate", endDate);
  313. (staffNos ?? []).forEach((no) => p.append("staffNo", no));
  314. const q = p.toString();
  315. const res = await clientAuthFetch(
  316. q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance`
  317. );
  318. if (!res.ok) throw new Error("Failed to fetch staff delivery performance");
  319. const data = await res.json();
  320. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => {
  321. // Accept camelCase or lowercase keys (JDBC/DB may return different casing)
  322. const row = r as Record<string, unknown>;
  323. return {
  324. date: String(row.date ?? row.Date ?? ""),
  325. staffName: String(row.staffName ?? row.staffname ?? ""),
  326. orderCount: Number(row.orderCount ?? row.ordercount ?? 0),
  327. totalMinutes: Number(row.totalMinutes ?? row.totalminutes ?? 0),
  328. };
  329. });
  330. }
  331. export async function fetchStockTransactionsByDate(
  332. startDate?: string,
  333. endDate?: string
  334. ): Promise<StockTransactionsByDateRow[]> {
  335. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  336. const res = await clientAuthFetch(`${BASE}/stock-transactions-by-date?${q}`);
  337. if (!res.ok) throw new Error("Failed to fetch stock transactions by date");
  338. const data = await res.json();
  339. return normalizeChartRows(data, "date", ["inQty", "outQty", "totalQty"]);
  340. }
  341. export async function fetchDeliveryOrderByDate(
  342. startDate?: string,
  343. endDate?: string
  344. ): Promise<DeliveryOrderByDateRow[]> {
  345. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  346. const res = await clientAuthFetch(`${BASE}/delivery-order-by-date?${q}`);
  347. if (!res.ok) throw new Error("Failed to fetch delivery order by date");
  348. const data = await res.json();
  349. return normalizeChartRows(data, "date", ["orderCount", "totalQty"]);
  350. }
  351. export async function fetchPurchaseOrderByStatus(
  352. targetDate?: string,
  353. filters?: PurchaseOrderChartFilters
  354. ): Promise<PurchaseOrderByStatusRow[]> {
  355. const p = new URLSearchParams();
  356. if (targetDate) p.set("targetDate", targetDate);
  357. appendPurchaseOrderListParams(p, filters);
  358. const q = p.toString();
  359. const res = await clientAuthFetch(
  360. q ? `${BASE}/purchase-order-by-status?${q}` : `${BASE}/purchase-order-by-status`
  361. );
  362. if (!res.ok) throw new Error("Failed to fetch purchase order by status");
  363. const data = await res.json();
  364. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  365. status: String(r.status ?? ""),
  366. count: Number(r.count ?? 0),
  367. }));
  368. }
  369. export async function fetchPurchaseOrderFilterOptions(
  370. targetDate?: string
  371. ): Promise<PurchaseOrderFilterOptions> {
  372. const p = new URLSearchParams();
  373. if (targetDate) p.set("targetDate", targetDate);
  374. const q = p.toString();
  375. const res = await clientAuthFetch(
  376. q ? `${BASE}/purchase-order-filter-options?${q}` : `${BASE}/purchase-order-filter-options`
  377. );
  378. if (!res.ok) throw new Error("Failed to fetch purchase order filter options");
  379. const data = await res.json();
  380. const row = (data ?? {}) as Record<string, unknown>;
  381. const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[];
  382. const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[];
  383. const poNos = (Array.isArray(row.poNos) ? row.poNos : []) as Record<string, unknown>[];
  384. return {
  385. suppliers: suppliers.map((r) => ({
  386. supplierId: Number(r.supplierId ?? r.supplierid ?? 0),
  387. code: String(r.code ?? ""),
  388. name: String(r.name ?? ""),
  389. })),
  390. items: items.map((r) => ({
  391. itemCode: String(r.itemCode ?? r.itemcode ?? ""),
  392. itemName: String(r.itemName ?? r.itemname ?? ""),
  393. })),
  394. poNos: poNos.map((r) => ({
  395. poNo: String(r.poNo ?? r.pono ?? ""),
  396. })),
  397. };
  398. }
  399. export async function fetchPurchaseOrderEstimatedArrivalSummary(
  400. targetDate?: string,
  401. filters?: PurchaseOrderChartFilters
  402. ): Promise<PurchaseOrderEstimatedArrivalRow[]> {
  403. const p = new URLSearchParams();
  404. if (targetDate) p.set("targetDate", targetDate);
  405. appendPurchaseOrderListParams(p, filters);
  406. const q = p.toString();
  407. const res = await clientAuthFetch(
  408. q
  409. ? `${BASE}/purchase-order-estimated-arrival-summary?${q}`
  410. : `${BASE}/purchase-order-estimated-arrival-summary`
  411. );
  412. if (!res.ok) throw new Error("Failed to fetch estimated arrival summary");
  413. const data = await res.json();
  414. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  415. bucket: String(r.bucket ?? ""),
  416. count: Number(r.count ?? 0),
  417. }));
  418. }
  419. export interface EstimatedArrivalBreakdownSupplierRow {
  420. supplierId: number | null;
  421. supplierCode: string;
  422. supplierName: string;
  423. poCount: number;
  424. }
  425. export interface EstimatedArrivalBreakdownItemRow {
  426. itemCode: string;
  427. itemName: string;
  428. poCount: number;
  429. totalQty: number;
  430. }
  431. export interface EstimatedArrivalBreakdownPoRow {
  432. purchaseOrderId: number;
  433. purchaseOrderNo: string;
  434. status: string;
  435. orderDate: string;
  436. supplierId: number | null;
  437. supplierCode: string;
  438. supplierName: string;
  439. }
  440. export interface PurchaseOrderEstimatedArrivalBreakdown {
  441. suppliers: EstimatedArrivalBreakdownSupplierRow[];
  442. items: EstimatedArrivalBreakdownItemRow[];
  443. purchaseOrders: EstimatedArrivalBreakdownPoRow[];
  444. }
  445. /** Related suppliers / items / POs for one 預計送貨 bucket (same bar filters as the donut). */
  446. export async function fetchPurchaseOrderEstimatedArrivalBreakdown(
  447. targetDate: string,
  448. estimatedArrivalBucket: string,
  449. filters?: PurchaseOrderChartFilters
  450. ): Promise<PurchaseOrderEstimatedArrivalBreakdown> {
  451. const p = new URLSearchParams();
  452. p.set("targetDate", targetDate);
  453. p.set("estimatedArrivalBucket", estimatedArrivalBucket.trim().toLowerCase());
  454. appendPurchaseOrderListParams(p, filters);
  455. const res = await clientAuthFetch(`${BASE}/purchase-order-estimated-arrival-breakdown?${p.toString()}`);
  456. if (!res.ok) throw new Error("Failed to fetch estimated arrival breakdown");
  457. const data = await res.json();
  458. const row = (data ?? {}) as Record<string, unknown>;
  459. const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[];
  460. const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[];
  461. const purchaseOrders = (Array.isArray(row.purchaseOrders) ? row.purchaseOrders : []) as Record<string, unknown>[];
  462. return {
  463. suppliers: suppliers.map((r) => ({
  464. supplierId: (() => {
  465. const v = r.supplierId ?? r.supplierid;
  466. if (v == null || v === "") return null;
  467. const n = Number(v);
  468. return Number.isFinite(n) ? n : null;
  469. })(),
  470. supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""),
  471. supplierName: String(r.supplierName ?? r.suppliername ?? ""),
  472. poCount: Number(r.poCount ?? r.pocount ?? 0),
  473. })),
  474. items: items.map((r) => ({
  475. itemCode: String(r.itemCode ?? r.itemcode ?? ""),
  476. itemName: String(r.itemName ?? r.itemname ?? ""),
  477. poCount: Number(r.poCount ?? r.pocount ?? 0),
  478. totalQty: Number(r.totalQty ?? r.totalqty ?? 0),
  479. })),
  480. purchaseOrders: purchaseOrders.map((r) => ({
  481. purchaseOrderId: Number(r.purchaseOrderId ?? r.purchaseorderid ?? 0),
  482. purchaseOrderNo: String(r.purchaseOrderNo ?? r.purchaseorderno ?? ""),
  483. status: String(r.status ?? ""),
  484. orderDate: String(r.orderDate ?? r.orderdate ?? ""),
  485. supplierId: (() => {
  486. const v = r.supplierId ?? r.supplierid;
  487. if (v == null || v === "") return null;
  488. const n = Number(v);
  489. return Number.isFinite(n) ? n : null;
  490. })(),
  491. supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""),
  492. supplierName: String(r.supplierName ?? r.suppliername ?? ""),
  493. })),
  494. };
  495. }
  496. export type PurchaseOrderDrillQuery = PurchaseOrderChartFilters & {
  497. /** order = PO order date; complete = PO complete date (for received/completed on a day) */
  498. dateFilter?: "order" | "complete";
  499. /** delivered | not_delivered | cancelled | other — same as 預計送貨 donut buckets */
  500. estimatedArrivalBucket?: string;
  501. };
  502. export async function fetchPurchaseOrderDetailsByStatus(
  503. status: string,
  504. targetDate?: string,
  505. opts?: PurchaseOrderDrillQuery
  506. ): Promise<PurchaseOrderDetailByStatusRow[]> {
  507. const p = new URLSearchParams();
  508. p.set("status", status.trim().toLowerCase());
  509. if (targetDate) p.set("targetDate", targetDate);
  510. if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter);
  511. if (opts?.estimatedArrivalBucket?.trim()) {
  512. p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase());
  513. }
  514. appendPurchaseOrderListParams(p, opts);
  515. const q = p.toString();
  516. const res = await clientAuthFetch(`${BASE}/purchase-order-details-by-status?${q}`);
  517. if (!res.ok) throw new Error("Failed to fetch purchase order details by status");
  518. const data = await res.json();
  519. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  520. purchaseOrderId: Number(r.purchaseOrderId ?? 0),
  521. purchaseOrderNo: String(r.purchaseOrderNo ?? ""),
  522. status: String(r.status ?? ""),
  523. orderDate: String(r.orderDate ?? ""),
  524. estimatedArrivalDate: String(r.estimatedArrivalDate ?? ""),
  525. supplierId: (() => {
  526. const v = r.supplierId;
  527. if (v == null || v === "") return null;
  528. const n = Number(v);
  529. return Number.isFinite(n) && n > 0 ? n : null;
  530. })(),
  531. supplierCode: String(r.supplierCode ?? ""),
  532. supplierName: String(r.supplierName ?? ""),
  533. itemCount: Number(r.itemCount ?? 0),
  534. totalQty: Number(r.totalQty ?? 0),
  535. }));
  536. }
  537. export async function fetchPurchaseOrderItems(
  538. purchaseOrderId: number
  539. ): Promise<PurchaseOrderItemRow[]> {
  540. const q = buildParams({ purchaseOrderId });
  541. const res = await clientAuthFetch(`${BASE}/purchase-order-items?${q}`);
  542. if (!res.ok) throw new Error("Failed to fetch purchase order items");
  543. const data = await res.json();
  544. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  545. purchaseOrderLineId: Number(r.purchaseOrderLineId ?? 0),
  546. itemCode: String(r.itemCode ?? ""),
  547. itemName: String(r.itemName ?? ""),
  548. orderedQty: Number(r.orderedQty ?? 0),
  549. uom: String(r.uom ?? ""),
  550. receivedQty: Number(r.receivedQty ?? 0),
  551. pendingQty: Number(r.pendingQty ?? 0),
  552. }));
  553. }
  554. export async function fetchPurchaseOrderItemsByStatus(
  555. status: string,
  556. targetDate?: string,
  557. opts?: PurchaseOrderDrillQuery
  558. ): Promise<PurchaseOrderItemRow[]> {
  559. const p = new URLSearchParams();
  560. p.set("status", status.trim().toLowerCase());
  561. if (targetDate) p.set("targetDate", targetDate);
  562. if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter);
  563. if (opts?.estimatedArrivalBucket?.trim()) {
  564. p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase());
  565. }
  566. appendPurchaseOrderListParams(p, opts);
  567. const q = p.toString();
  568. const res = await clientAuthFetch(`${BASE}/purchase-order-items-by-status?${q}`);
  569. if (!res.ok) throw new Error("Failed to fetch purchase order items by status");
  570. const data = await res.json();
  571. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  572. purchaseOrderLineId: 0,
  573. itemCode: String(r.itemCode ?? ""),
  574. itemName: String(r.itemName ?? ""),
  575. orderedQty: Number(r.orderedQty ?? 0),
  576. uom: String(r.uom ?? ""),
  577. receivedQty: Number(r.receivedQty ?? 0),
  578. pendingQty: Number(r.pendingQty ?? 0),
  579. }));
  580. }
  581. export async function fetchStockInOutByDate(
  582. startDate?: string,
  583. endDate?: string
  584. ): Promise<StockInOutByDateRow[]> {
  585. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  586. const res = await clientAuthFetch(`${BASE}/stock-in-out-by-date?${q}`);
  587. if (!res.ok) throw new Error("Failed to fetch stock in/out by date");
  588. const data = await res.json();
  589. return normalizeChartRows(data, "date", ["inQty", "outQty"]);
  590. }
  591. export interface TopDeliveryItemOption {
  592. itemCode: string;
  593. itemName: string;
  594. }
  595. export async function fetchTopDeliveryItemsItemOptions(
  596. startDate?: string,
  597. endDate?: string
  598. ): Promise<TopDeliveryItemOption[]> {
  599. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  600. const res = await clientAuthFetch(
  601. q ? `${BASE}/top-delivery-items-item-options?${q}` : `${BASE}/top-delivery-items-item-options`
  602. );
  603. if (!res.ok) throw new Error("Failed to fetch item options");
  604. const data = await res.json();
  605. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  606. itemCode: String(r.itemCode ?? ""),
  607. itemName: String(r.itemName ?? ""),
  608. }));
  609. }
  610. export async function fetchTopDeliveryItems(
  611. startDate?: string,
  612. endDate?: string,
  613. limit = 10,
  614. itemCodes?: string[]
  615. ): Promise<TopDeliveryItemsRow[]> {
  616. const p = new URLSearchParams();
  617. if (startDate) p.set("startDate", startDate);
  618. if (endDate) p.set("endDate", endDate);
  619. p.set("limit", String(limit));
  620. (itemCodes ?? []).forEach((code) => p.append("itemCode", code));
  621. const q = p.toString();
  622. const res = await clientAuthFetch(`${BASE}/top-delivery-items?${q}`);
  623. if (!res.ok) throw new Error("Failed to fetch top delivery items");
  624. const data = await res.json();
  625. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  626. itemCode: String(r.itemCode ?? ""),
  627. itemName: String(r.itemName ?? ""),
  628. totalQty: Number(r.totalQty ?? 0),
  629. }));
  630. }
  631. export async function fetchStockBalanceTrend(
  632. startDate?: string,
  633. endDate?: string,
  634. itemCode?: string
  635. ): Promise<StockBalanceTrendRow[]> {
  636. const q = buildParams({
  637. startDate: startDate ?? "",
  638. endDate: endDate ?? "",
  639. itemCode: itemCode ?? "",
  640. });
  641. const res = await clientAuthFetch(`${BASE}/stock-balance-trend?${q}`);
  642. if (!res.ok) throw new Error("Failed to fetch stock balance trend");
  643. const data = await res.json();
  644. return normalizeChartRows(data, "date", ["balance"]);
  645. }
  646. export async function fetchConsumptionTrendByMonth(
  647. year?: number,
  648. startDate?: string,
  649. endDate?: string,
  650. itemCode?: string
  651. ): Promise<ConsumptionTrendByMonthRow[]> {
  652. const q = buildParams({
  653. year: year ?? "",
  654. startDate: startDate ?? "",
  655. endDate: endDate ?? "",
  656. itemCode: itemCode ?? "",
  657. });
  658. const res = await clientAuthFetch(`${BASE}/consumption-trend-by-month?${q}`);
  659. if (!res.ok) throw new Error("Failed to fetch consumption trend");
  660. const data = await res.json();
  661. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  662. month: String(r.month ?? ""),
  663. outQty: Number(r.outQty ?? 0),
  664. }));
  665. }
  666. /** Normalize rows: ensure date key is string and numeric keys are numbers (backend may return BigDecimal/Long). */
  667. function normalizeChartRows<T>(
  668. rows: unknown[],
  669. dateKey: string,
  670. numberKeys: string[]
  671. ): T[] {
  672. if (!Array.isArray(rows)) return [];
  673. return rows.map((r: unknown) => {
  674. const row = r as Record<string, unknown>;
  675. const out: Record<string, unknown> = {};
  676. out[dateKey] = row[dateKey] != null ? String(row[dateKey]) : "";
  677. numberKeys.forEach((k) => {
  678. out[k] = Number(row[k]) || 0;
  679. });
  680. return out as T;
  681. });
  682. }