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.
 
 

976 line
36 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 interface JobOrderBoardRow {
  252. jobOrderId: number;
  253. code: string;
  254. status: string;
  255. planStart: string;
  256. actualStart: string;
  257. planEnd: string;
  258. actualEnd: string;
  259. materialPendingCount: number;
  260. materialPickedCount: number;
  261. processTotalCount: number;
  262. processCompletedCount: number;
  263. currentProcessCode: string;
  264. currentProcessName: string;
  265. currentProcessStartTime: string;
  266. /** FG/WIP job stock-in: sum acceptedQty on all linked lines */
  267. stockInAcceptedQtyTotal: number;
  268. /** Lines QC-passed, waiting putaway (receiving / received) */
  269. fgReadyToStockInCount: number;
  270. fgReadyToStockInQty: number;
  271. fgInQcLineCount: number;
  272. fgInQcQty: number;
  273. fgStockedQty: number;
  274. /** Same sources as /jo/edit 工藝流程 summary (product process + lines) */
  275. itemCode: string;
  276. itemName: string;
  277. jobTypeName: string;
  278. reqQty: number;
  279. outputQtyUom: string;
  280. productionDate: string;
  281. /** Sum of line processingTime (matches ProcessSummaryHeader 預計所需時間) */
  282. planProcessingMinsTotal: number;
  283. /** Sum of setup + changeover minutes on all lines */
  284. planSetupChangeoverMinsTotal: number;
  285. productProcessStart: string;
  286. /** Σ line durations in decimal minutes (seconds÷60); sub-minute shown; Pass w/o endTime uses planned processing min */
  287. actualLineMinsTotal: number;
  288. }
  289. function numField(v: unknown): number {
  290. if (v == null || v === "") return 0;
  291. const n = Number(v);
  292. return Number.isFinite(n) ? n : 0;
  293. }
  294. function mapJobOrderBoardRow(r: Record<string, unknown>): JobOrderBoardRow {
  295. const id = r.jobOrderId ?? r.joborderid;
  296. return {
  297. jobOrderId: Number(id ?? 0),
  298. code: String(r.code ?? ""),
  299. status: String(r.status ?? ""),
  300. planStart: String(r.planStart ?? r.planstart ?? ""),
  301. actualStart: String(r.actualStart ?? r.actualstart ?? ""),
  302. planEnd: String(r.planEnd ?? r.planend ?? ""),
  303. actualEnd: String(r.actualEnd ?? r.actualend ?? ""),
  304. materialPendingCount: Number(r.materialPendingCount ?? r.materialpendingcount ?? 0),
  305. materialPickedCount: Number(r.materialPickedCount ?? r.materialpickedcount ?? 0),
  306. processTotalCount: Number(r.processTotalCount ?? r.processtotalcount ?? 0),
  307. processCompletedCount: Number(r.processCompletedCount ?? r.processcompletedcount ?? 0),
  308. currentProcessCode: String(r.currentProcessCode ?? r.currentprocesscode ?? ""),
  309. currentProcessName: String(r.currentProcessName ?? r.currentprocessname ?? ""),
  310. currentProcessStartTime: String(r.currentProcessStartTime ?? r.currentprocessstarttime ?? ""),
  311. stockInAcceptedQtyTotal: Number(r.stockInAcceptedQtyTotal ?? r.stockinacceptedqtytotal ?? 0),
  312. fgReadyToStockInCount: Number(r.fgReadyToStockInCount ?? r.fgreadytostockincount ?? 0),
  313. fgReadyToStockInQty: Number(r.fgReadyToStockInQty ?? r.fgreadytostockinqty ?? 0),
  314. fgInQcLineCount: Number(r.fgInQcLineCount ?? r.fginqclinecount ?? 0),
  315. fgInQcQty: Number(r.fgInQcQty ?? r.fginqcqty ?? 0),
  316. fgStockedQty: Number(r.fgStockedQty ?? r.fgstockedqty ?? 0),
  317. itemCode: String(r.itemCode ?? r.itemcode ?? ""),
  318. itemName: String(r.itemName ?? r.itemname ?? ""),
  319. jobTypeName: String(r.jobTypeName ?? r.jobtypename ?? ""),
  320. reqQty: numField(r.reqQty ?? r.reqqty),
  321. outputQtyUom: String(r.outputQtyUom ?? r.outputqtyuom ?? ""),
  322. productionDate: String(r.productionDate ?? r.productiondate ?? ""),
  323. planProcessingMinsTotal: numField(r.planProcessingMinsTotal ?? r.planprocessingminstotal),
  324. planSetupChangeoverMinsTotal: numField(r.planSetupChangeoverMinsTotal ?? r.plansetupchangeoverminstotal),
  325. productProcessStart: String(r.productProcessStart ?? r.productprocessstart ?? ""),
  326. actualLineMinsTotal: numField(r.actualLineMinsTotal ?? r.actuallineminstotal),
  327. };
  328. }
  329. /** Per-job board rows. With [incompleteOnly], excludes status completed (backend LOWER(status) <> 'completed'). */
  330. export async function fetchJobOrderBoard(
  331. targetDate?: string,
  332. opts?: { incompleteOnly?: boolean },
  333. ): Promise<JobOrderBoardRow[]> {
  334. const params: Record<string, string | number | undefined> = {};
  335. if (targetDate) params.targetDate = targetDate;
  336. if (opts?.incompleteOnly) params.incompleteOnly = "true";
  337. const q = buildParams(params);
  338. const res = await clientAuthFetch(q ? `${BASE}/job-order-board?${q}` : `${BASE}/job-order-board`);
  339. if (!res.ok) throw new Error("Failed to fetch job order board");
  340. const data = await res.json();
  341. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map(mapJobOrderBoardRow);
  342. }
  343. export interface ProcessBoardRow {
  344. jopId: number;
  345. jobOrderId: number;
  346. jobOrderCode: string;
  347. jobOrderStatus: string;
  348. processId: number;
  349. processCode: string;
  350. processName: string;
  351. seqNo: number;
  352. rowStatus: string;
  353. jobPlanStart: string;
  354. startTime: string;
  355. endTime: string;
  356. /** Derived: pending | in_progress | completed */
  357. boardStatus: string;
  358. /** 工藝流程步驟名稱(productprocessline.name;多筆以 | 分隔);無明細時為主檔工序名。 */
  359. lineStepName: string;
  360. /** 描述 */
  361. lineDescription: string;
  362. /** 設備類型-設備名稱-編號(與工單工藝流程一致) */
  363. lineEquipmentLabel: string;
  364. /** 操作員/員工顯示名 */
  365. lineOperatorInfo: string;
  366. itemCode: string;
  367. itemName: string;
  368. jobTypeName: string;
  369. reqQty: number;
  370. outputQtyUom: string;
  371. productionDate: string;
  372. planProcessingMinsTotal: number;
  373. planSetupChangeoverMinsTotal: number;
  374. productProcessStart: string;
  375. actualLineMinsTotal: number;
  376. /** This BOM step: sum(processing+setup+changeover) on matching lines */
  377. stepPlanMins: number;
  378. /** This BOM step: Σ line durations in decimal minutes (seconds÷60); Pass/Completed without endTime uses planned processing min as fallback */
  379. stepActualMins: number;
  380. }
  381. function mapProcessBoardRow(r: Record<string, unknown>): ProcessBoardRow {
  382. return {
  383. jopId: Number(r.jopId ?? r.jopid ?? 0),
  384. jobOrderId: Number(r.jobOrderId ?? r.joborderid ?? 0),
  385. jobOrderCode: String(r.jobOrderCode ?? r.jobordercode ?? ""),
  386. jobOrderStatus: String(r.jobOrderStatus ?? r.joborderstatus ?? ""),
  387. processId: Number(r.processId ?? r.processid ?? 0),
  388. processCode: String(r.processCode ?? r.processcode ?? ""),
  389. processName: String(r.processName ?? r.processname ?? ""),
  390. seqNo: Number(r.seqNo ?? r.seqno ?? 0),
  391. rowStatus: String(r.rowStatus ?? r.rowstatus ?? ""),
  392. jobPlanStart: String(r.jobPlanStart ?? r.jobplanstart ?? ""),
  393. startTime: String(r.startTime ?? r.starttime ?? ""),
  394. endTime: String(r.endTime ?? r.endtime ?? ""),
  395. boardStatus: String(r.boardStatus ?? r.boardstatus ?? "pending").toLowerCase(),
  396. lineStepName: String(r.lineStepName ?? r.linestepname ?? r.line_step_name ?? ""),
  397. lineDescription: String(r.lineDescription ?? r.linedescription ?? r.line_description ?? ""),
  398. lineEquipmentLabel: String(r.lineEquipmentLabel ?? r.lineequipmentlabel ?? r.line_equipment_label ?? ""),
  399. lineOperatorInfo: String(r.lineOperatorInfo ?? r.lineoperatorinfo ?? r.line_operator_info ?? ""),
  400. itemCode: String(r.itemCode ?? r.itemcode ?? ""),
  401. itemName: String(r.itemName ?? r.itemname ?? ""),
  402. jobTypeName: String(r.jobTypeName ?? r.jobtypename ?? ""),
  403. reqQty: numField(r.reqQty ?? r.reqqty),
  404. outputQtyUom: String(r.outputQtyUom ?? r.outputqtyuom ?? ""),
  405. productionDate: String(r.productionDate ?? r.productiondate ?? ""),
  406. planProcessingMinsTotal: numField(r.planProcessingMinsTotal ?? r.planprocessingminstotal),
  407. planSetupChangeoverMinsTotal: numField(r.planSetupChangeoverMinsTotal ?? r.plansetupchangeoverminstotal),
  408. productProcessStart: String(r.productProcessStart ?? r.productprocessstart ?? ""),
  409. actualLineMinsTotal: numField(r.actualLineMinsTotal ?? r.actuallineminstotal),
  410. stepPlanMins: numField(r.stepPlanMins ?? r.stepplanmins),
  411. stepActualMins: numField(r.stepActualMins ?? r.stepactualmins),
  412. };
  413. }
  414. /** Per job_order_process line; same filters as job-order board. */
  415. export async function fetchProcessBoard(
  416. targetDate?: string,
  417. opts?: { incompleteOnly?: boolean },
  418. ): Promise<ProcessBoardRow[]> {
  419. const params: Record<string, string | number | undefined> = {};
  420. if (targetDate) params.targetDate = targetDate;
  421. if (opts?.incompleteOnly) params.incompleteOnly = "true";
  422. const q = buildParams(params);
  423. const res = await clientAuthFetch(q ? `${BASE}/process-board?${q}` : `${BASE}/process-board`);
  424. if (!res.ok) throw new Error("Failed to fetch process board");
  425. const data = await res.json();
  426. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map(mapProcessBoardRow);
  427. }
  428. export interface EquipmentUsageBoardRow {
  429. jopdId: number;
  430. equipmentId: number;
  431. equipmentCode: string;
  432. equipmentName: string;
  433. jobOrderId: number;
  434. jobOrderCode: string;
  435. jobPlanStart: string;
  436. processCode: string;
  437. processName: string;
  438. operatingStart: string;
  439. operatingEnd: string;
  440. /** Estimated usage minutes (start–end diff, or 產線 processingTime when Pass/Completed without end). */
  441. usageMinutes: number;
  442. workingNow: number;
  443. operatorUsername: string;
  444. operatorName: string;
  445. }
  446. function mapEquipmentUsageBoardRow(r: Record<string, unknown>): EquipmentUsageBoardRow {
  447. return {
  448. jopdId: Number(r.jopdId ?? r.jopdid ?? 0),
  449. equipmentId: Number(r.equipmentId ?? r.equipmentid ?? 0),
  450. equipmentCode: String(r.equipmentCode ?? r.equipmentcode ?? ""),
  451. equipmentName: String(r.equipmentName ?? r.equipmentname ?? ""),
  452. jobOrderId: Number(r.jobOrderId ?? r.joborderid ?? 0),
  453. jobOrderCode: String(r.jobOrderCode ?? r.jobordercode ?? ""),
  454. jobPlanStart: String(r.jobPlanStart ?? r.jobplanstart ?? ""),
  455. processCode: String(r.processCode ?? r.processcode ?? ""),
  456. processName: String(r.processName ?? r.processname ?? ""),
  457. operatingStart: String(r.operatingStart ?? r.operatingstart ?? ""),
  458. operatingEnd: String(r.operatingEnd ?? r.operatingend ?? ""),
  459. usageMinutes: Number(r.usageMinutes ?? r.usageminutes ?? 0),
  460. workingNow: Number(r.workingNow ?? r.workingnow ?? 0),
  461. operatorUsername: String(r.operatorUsername ?? r.operatorusername ?? ""),
  462. operatorName: String(r.operatorName ?? r.operatorname ?? ""),
  463. };
  464. }
  465. /** Day = COALESCE(line/jopd times, jop.endTime, planStart). Includes productprocessline (工藝流程) and job_order_process_detail. Omit targetDate = server today. */
  466. export async function fetchEquipmentUsageBoard(targetDate?: string): Promise<EquipmentUsageBoardRow[]> {
  467. const q = buildParams({ targetDate: targetDate ?? "" });
  468. const res = await clientAuthFetch(q ? `${BASE}/equipment-usage-board?${q}` : `${BASE}/equipment-usage-board`);
  469. if (!res.ok) throw new Error("Failed to fetch equipment usage board");
  470. const data = await res.json();
  471. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map(mapEquipmentUsageBoardRow);
  472. }
  473. export async function fetchProductionScheduleByDate(
  474. startDate?: string,
  475. endDate?: string
  476. ): Promise<ProductionScheduleByDateRow[]> {
  477. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  478. const res = await clientAuthFetch(
  479. `${BASE}/production-schedule-by-date?${q}`
  480. );
  481. if (!res.ok) throw new Error("Failed to fetch production schedule by date");
  482. const data = await res.json();
  483. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  484. date: String(r.date ?? ""),
  485. scheduledItemCount: Number(r.scheduledItemCount ?? r.scheduleCount ?? 0),
  486. totalEstProdCount: Number(r.totalEstProdCount ?? 0),
  487. }));
  488. }
  489. export async function fetchPlannedDailyOutputByItem(
  490. limit = 20
  491. ): Promise<PlannedDailyOutputRow[]> {
  492. const res = await clientAuthFetch(
  493. `${BASE}/planned-daily-output-by-item?limit=${limit}`
  494. );
  495. if (!res.ok) throw new Error("Failed to fetch planned daily output");
  496. const data = await res.json();
  497. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  498. itemCode: String(r.itemCode ?? ""),
  499. itemName: String(r.itemName ?? ""),
  500. dailyQty: Number(r.dailyQty ?? 0),
  501. }));
  502. }
  503. /** Planned production by date and by item (production_schedule). */
  504. export interface PlannedOutputByDateAndItemRow {
  505. date: string;
  506. itemCode: string;
  507. itemName: string;
  508. qty: number;
  509. }
  510. export async function fetchPlannedOutputByDateAndItem(
  511. startDate?: string,
  512. endDate?: string
  513. ): Promise<PlannedOutputByDateAndItemRow[]> {
  514. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  515. const res = await clientAuthFetch(
  516. q ? `${BASE}/planned-output-by-date-and-item?${q}` : `${BASE}/planned-output-by-date-and-item`
  517. );
  518. if (!res.ok) throw new Error("Failed to fetch planned output by date and item");
  519. const data = await res.json();
  520. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  521. date: String(r.date ?? ""),
  522. itemCode: String(r.itemCode ?? ""),
  523. itemName: String(r.itemName ?? ""),
  524. qty: Number(r.qty ?? 0),
  525. }));
  526. }
  527. export async function fetchStaffDeliveryPerformance(
  528. startDate?: string,
  529. endDate?: string,
  530. staffNos?: string[]
  531. ): Promise<StaffDeliveryPerformanceRow[]> {
  532. const p = new URLSearchParams();
  533. if (startDate) p.set("startDate", startDate);
  534. if (endDate) p.set("endDate", endDate);
  535. (staffNos ?? []).forEach((no) => p.append("staffNo", no));
  536. const q = p.toString();
  537. const res = await clientAuthFetch(
  538. q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance`
  539. );
  540. if (!res.ok) throw new Error("Failed to fetch staff delivery performance");
  541. const data = await res.json();
  542. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => {
  543. // Accept camelCase or lowercase keys (JDBC/DB may return different casing)
  544. const row = r as Record<string, unknown>;
  545. return {
  546. date: String(row.date ?? row.Date ?? ""),
  547. staffName: String(row.staffName ?? row.staffname ?? ""),
  548. orderCount: Number(row.orderCount ?? row.ordercount ?? 0),
  549. totalMinutes: Number(row.totalMinutes ?? row.totalminutes ?? 0),
  550. };
  551. });
  552. }
  553. export async function fetchStockTransactionsByDate(
  554. startDate?: string,
  555. endDate?: string
  556. ): Promise<StockTransactionsByDateRow[]> {
  557. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  558. const res = await clientAuthFetch(`${BASE}/stock-transactions-by-date?${q}`);
  559. if (!res.ok) throw new Error("Failed to fetch stock transactions by date");
  560. const data = await res.json();
  561. return normalizeChartRows(data, "date", ["inQty", "outQty", "totalQty"]);
  562. }
  563. export async function fetchDeliveryOrderByDate(
  564. startDate?: string,
  565. endDate?: string
  566. ): Promise<DeliveryOrderByDateRow[]> {
  567. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  568. const res = await clientAuthFetch(`${BASE}/delivery-order-by-date?${q}`);
  569. if (!res.ok) throw new Error("Failed to fetch delivery order by date");
  570. const data = await res.json();
  571. return normalizeChartRows(data, "date", ["orderCount", "totalQty"]);
  572. }
  573. export async function fetchPurchaseOrderByStatus(
  574. targetDate?: string,
  575. filters?: PurchaseOrderChartFilters
  576. ): Promise<PurchaseOrderByStatusRow[]> {
  577. const p = new URLSearchParams();
  578. if (targetDate) p.set("targetDate", targetDate);
  579. appendPurchaseOrderListParams(p, filters);
  580. const q = p.toString();
  581. const res = await clientAuthFetch(
  582. q ? `${BASE}/purchase-order-by-status?${q}` : `${BASE}/purchase-order-by-status`
  583. );
  584. if (!res.ok) throw new Error("Failed to fetch purchase order by status");
  585. const data = await res.json();
  586. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  587. status: String(r.status ?? ""),
  588. count: Number(r.count ?? 0),
  589. }));
  590. }
  591. export async function fetchPurchaseOrderFilterOptions(
  592. targetDate?: string
  593. ): Promise<PurchaseOrderFilterOptions> {
  594. const p = new URLSearchParams();
  595. if (targetDate) p.set("targetDate", targetDate);
  596. const q = p.toString();
  597. const res = await clientAuthFetch(
  598. q ? `${BASE}/purchase-order-filter-options?${q}` : `${BASE}/purchase-order-filter-options`
  599. );
  600. if (!res.ok) throw new Error("Failed to fetch purchase order filter options");
  601. const data = await res.json();
  602. const row = (data ?? {}) as Record<string, unknown>;
  603. const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[];
  604. const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[];
  605. const poNos = (Array.isArray(row.poNos) ? row.poNos : []) as Record<string, unknown>[];
  606. return {
  607. suppliers: suppliers.map((r) => ({
  608. supplierId: Number(r.supplierId ?? r.supplierid ?? 0),
  609. code: String(r.code ?? ""),
  610. name: String(r.name ?? ""),
  611. })),
  612. items: items.map((r) => ({
  613. itemCode: String(r.itemCode ?? r.itemcode ?? ""),
  614. itemName: String(r.itemName ?? r.itemname ?? ""),
  615. })),
  616. poNos: poNos.map((r) => ({
  617. poNo: String(r.poNo ?? r.pono ?? ""),
  618. })),
  619. };
  620. }
  621. export async function fetchPurchaseOrderEstimatedArrivalSummary(
  622. targetDate?: string,
  623. filters?: PurchaseOrderChartFilters
  624. ): Promise<PurchaseOrderEstimatedArrivalRow[]> {
  625. const p = new URLSearchParams();
  626. if (targetDate) p.set("targetDate", targetDate);
  627. appendPurchaseOrderListParams(p, filters);
  628. const q = p.toString();
  629. const res = await clientAuthFetch(
  630. q
  631. ? `${BASE}/purchase-order-estimated-arrival-summary?${q}`
  632. : `${BASE}/purchase-order-estimated-arrival-summary`
  633. );
  634. if (!res.ok) throw new Error("Failed to fetch estimated arrival summary");
  635. const data = await res.json();
  636. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  637. bucket: String(r.bucket ?? ""),
  638. count: Number(r.count ?? 0),
  639. }));
  640. }
  641. export interface EstimatedArrivalBreakdownSupplierRow {
  642. supplierId: number | null;
  643. supplierCode: string;
  644. supplierName: string;
  645. poCount: number;
  646. }
  647. export interface EstimatedArrivalBreakdownItemRow {
  648. itemCode: string;
  649. itemName: string;
  650. poCount: number;
  651. totalQty: number;
  652. }
  653. export interface EstimatedArrivalBreakdownPoRow {
  654. purchaseOrderId: number;
  655. purchaseOrderNo: string;
  656. status: string;
  657. orderDate: string;
  658. supplierId: number | null;
  659. supplierCode: string;
  660. supplierName: string;
  661. }
  662. export interface PurchaseOrderEstimatedArrivalBreakdown {
  663. suppliers: EstimatedArrivalBreakdownSupplierRow[];
  664. items: EstimatedArrivalBreakdownItemRow[];
  665. purchaseOrders: EstimatedArrivalBreakdownPoRow[];
  666. }
  667. /** Related suppliers / items / POs for one 預計送貨 bucket (same bar filters as the donut). */
  668. export async function fetchPurchaseOrderEstimatedArrivalBreakdown(
  669. targetDate: string,
  670. estimatedArrivalBucket: string,
  671. filters?: PurchaseOrderChartFilters
  672. ): Promise<PurchaseOrderEstimatedArrivalBreakdown> {
  673. const p = new URLSearchParams();
  674. p.set("targetDate", targetDate);
  675. p.set("estimatedArrivalBucket", estimatedArrivalBucket.trim().toLowerCase());
  676. appendPurchaseOrderListParams(p, filters);
  677. const res = await clientAuthFetch(`${BASE}/purchase-order-estimated-arrival-breakdown?${p.toString()}`);
  678. if (!res.ok) throw new Error("Failed to fetch estimated arrival breakdown");
  679. const data = await res.json();
  680. const row = (data ?? {}) as Record<string, unknown>;
  681. const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[];
  682. const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[];
  683. const purchaseOrders = (Array.isArray(row.purchaseOrders) ? row.purchaseOrders : []) as Record<string, unknown>[];
  684. return {
  685. suppliers: suppliers.map((r) => ({
  686. supplierId: (() => {
  687. const v = r.supplierId ?? r.supplierid;
  688. if (v == null || v === "") return null;
  689. const n = Number(v);
  690. return Number.isFinite(n) ? n : null;
  691. })(),
  692. supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""),
  693. supplierName: String(r.supplierName ?? r.suppliername ?? ""),
  694. poCount: Number(r.poCount ?? r.pocount ?? 0),
  695. })),
  696. items: items.map((r) => ({
  697. itemCode: String(r.itemCode ?? r.itemcode ?? ""),
  698. itemName: String(r.itemName ?? r.itemname ?? ""),
  699. poCount: Number(r.poCount ?? r.pocount ?? 0),
  700. totalQty: Number(r.totalQty ?? r.totalqty ?? 0),
  701. })),
  702. purchaseOrders: purchaseOrders.map((r) => ({
  703. purchaseOrderId: Number(r.purchaseOrderId ?? r.purchaseorderid ?? 0),
  704. purchaseOrderNo: String(r.purchaseOrderNo ?? r.purchaseorderno ?? ""),
  705. status: String(r.status ?? ""),
  706. orderDate: String(r.orderDate ?? r.orderdate ?? ""),
  707. supplierId: (() => {
  708. const v = r.supplierId ?? r.supplierid;
  709. if (v == null || v === "") return null;
  710. const n = Number(v);
  711. return Number.isFinite(n) ? n : null;
  712. })(),
  713. supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""),
  714. supplierName: String(r.supplierName ?? r.suppliername ?? ""),
  715. })),
  716. };
  717. }
  718. export type PurchaseOrderDrillQuery = PurchaseOrderChartFilters & {
  719. /** order = PO order date; complete = PO complete date (for received/completed on a day) */
  720. dateFilter?: "order" | "complete";
  721. /** delivered | not_delivered | cancelled | other — same as 預計送貨 donut buckets */
  722. estimatedArrivalBucket?: string;
  723. };
  724. export async function fetchPurchaseOrderDetailsByStatus(
  725. status: string,
  726. targetDate?: string,
  727. opts?: PurchaseOrderDrillQuery
  728. ): Promise<PurchaseOrderDetailByStatusRow[]> {
  729. const p = new URLSearchParams();
  730. p.set("status", status.trim().toLowerCase());
  731. if (targetDate) p.set("targetDate", targetDate);
  732. if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter);
  733. if (opts?.estimatedArrivalBucket?.trim()) {
  734. p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase());
  735. }
  736. appendPurchaseOrderListParams(p, opts);
  737. const q = p.toString();
  738. const res = await clientAuthFetch(`${BASE}/purchase-order-details-by-status?${q}`);
  739. if (!res.ok) throw new Error("Failed to fetch purchase order details by status");
  740. const data = await res.json();
  741. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  742. purchaseOrderId: Number(r.purchaseOrderId ?? 0),
  743. purchaseOrderNo: String(r.purchaseOrderNo ?? ""),
  744. status: String(r.status ?? ""),
  745. orderDate: String(r.orderDate ?? ""),
  746. estimatedArrivalDate: String(r.estimatedArrivalDate ?? ""),
  747. supplierId: (() => {
  748. const v = r.supplierId;
  749. if (v == null || v === "") return null;
  750. const n = Number(v);
  751. return Number.isFinite(n) && n > 0 ? n : null;
  752. })(),
  753. supplierCode: String(r.supplierCode ?? ""),
  754. supplierName: String(r.supplierName ?? ""),
  755. itemCount: Number(r.itemCount ?? 0),
  756. totalQty: Number(r.totalQty ?? 0),
  757. }));
  758. }
  759. export async function fetchPurchaseOrderItems(
  760. purchaseOrderId: number
  761. ): Promise<PurchaseOrderItemRow[]> {
  762. const q = buildParams({ purchaseOrderId });
  763. const res = await clientAuthFetch(`${BASE}/purchase-order-items?${q}`);
  764. if (!res.ok) throw new Error("Failed to fetch purchase order items");
  765. const data = await res.json();
  766. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  767. purchaseOrderLineId: Number(r.purchaseOrderLineId ?? 0),
  768. itemCode: String(r.itemCode ?? ""),
  769. itemName: String(r.itemName ?? ""),
  770. orderedQty: Number(r.orderedQty ?? 0),
  771. uom: String(r.uom ?? ""),
  772. receivedQty: Number(r.receivedQty ?? 0),
  773. pendingQty: Number(r.pendingQty ?? 0),
  774. }));
  775. }
  776. export async function fetchPurchaseOrderItemsByStatus(
  777. status: string,
  778. targetDate?: string,
  779. opts?: PurchaseOrderDrillQuery
  780. ): Promise<PurchaseOrderItemRow[]> {
  781. const p = new URLSearchParams();
  782. p.set("status", status.trim().toLowerCase());
  783. if (targetDate) p.set("targetDate", targetDate);
  784. if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter);
  785. if (opts?.estimatedArrivalBucket?.trim()) {
  786. p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase());
  787. }
  788. appendPurchaseOrderListParams(p, opts);
  789. const q = p.toString();
  790. const res = await clientAuthFetch(`${BASE}/purchase-order-items-by-status?${q}`);
  791. if (!res.ok) throw new Error("Failed to fetch purchase order items by status");
  792. const data = await res.json();
  793. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  794. purchaseOrderLineId: 0,
  795. itemCode: String(r.itemCode ?? ""),
  796. itemName: String(r.itemName ?? ""),
  797. orderedQty: Number(r.orderedQty ?? 0),
  798. uom: String(r.uom ?? ""),
  799. receivedQty: Number(r.receivedQty ?? 0),
  800. pendingQty: Number(r.pendingQty ?? 0),
  801. }));
  802. }
  803. export async function fetchStockInOutByDate(
  804. startDate?: string,
  805. endDate?: string
  806. ): Promise<StockInOutByDateRow[]> {
  807. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  808. const res = await clientAuthFetch(`${BASE}/stock-in-out-by-date?${q}`);
  809. if (!res.ok) throw new Error("Failed to fetch stock in/out by date");
  810. const data = await res.json();
  811. return normalizeChartRows(data, "date", ["inQty", "outQty"]);
  812. }
  813. export interface TopDeliveryItemOption {
  814. itemCode: string;
  815. itemName: string;
  816. }
  817. export async function fetchTopDeliveryItemsItemOptions(
  818. startDate?: string,
  819. endDate?: string
  820. ): Promise<TopDeliveryItemOption[]> {
  821. const q = buildParams({ startDate: startDate ?? "", endDate: endDate ?? "" });
  822. const res = await clientAuthFetch(
  823. q ? `${BASE}/top-delivery-items-item-options?${q}` : `${BASE}/top-delivery-items-item-options`
  824. );
  825. if (!res.ok) throw new Error("Failed to fetch item options");
  826. const data = await res.json();
  827. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  828. itemCode: String(r.itemCode ?? ""),
  829. itemName: String(r.itemName ?? ""),
  830. }));
  831. }
  832. export async function fetchTopDeliveryItems(
  833. startDate?: string,
  834. endDate?: string,
  835. limit = 10,
  836. itemCodes?: string[]
  837. ): Promise<TopDeliveryItemsRow[]> {
  838. const p = new URLSearchParams();
  839. if (startDate) p.set("startDate", startDate);
  840. if (endDate) p.set("endDate", endDate);
  841. p.set("limit", String(limit));
  842. (itemCodes ?? []).forEach((code) => p.append("itemCode", code));
  843. const q = p.toString();
  844. const res = await clientAuthFetch(`${BASE}/top-delivery-items?${q}`);
  845. if (!res.ok) throw new Error("Failed to fetch top delivery items");
  846. const data = await res.json();
  847. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  848. itemCode: String(r.itemCode ?? ""),
  849. itemName: String(r.itemName ?? ""),
  850. totalQty: Number(r.totalQty ?? 0),
  851. }));
  852. }
  853. export async function fetchStockBalanceTrend(
  854. startDate?: string,
  855. endDate?: string,
  856. itemCode?: string
  857. ): Promise<StockBalanceTrendRow[]> {
  858. const q = buildParams({
  859. startDate: startDate ?? "",
  860. endDate: endDate ?? "",
  861. itemCode: itemCode ?? "",
  862. });
  863. const res = await clientAuthFetch(`${BASE}/stock-balance-trend?${q}`);
  864. if (!res.ok) throw new Error("Failed to fetch stock balance trend");
  865. const data = await res.json();
  866. return normalizeChartRows(data, "date", ["balance"]);
  867. }
  868. export async function fetchConsumptionTrendByMonth(
  869. year?: number,
  870. startDate?: string,
  871. endDate?: string,
  872. itemCode?: string
  873. ): Promise<ConsumptionTrendByMonthRow[]> {
  874. const q = buildParams({
  875. year: year ?? "",
  876. startDate: startDate ?? "",
  877. endDate: endDate ?? "",
  878. itemCode: itemCode ?? "",
  879. });
  880. const res = await clientAuthFetch(`${BASE}/consumption-trend-by-month?${q}`);
  881. if (!res.ok) throw new Error("Failed to fetch consumption trend");
  882. const data = await res.json();
  883. return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
  884. month: String(r.month ?? ""),
  885. outQty: Number(r.outQty ?? 0),
  886. }));
  887. }
  888. /** Normalize rows: ensure date key is string and numeric keys are numbers (backend may return BigDecimal/Long). */
  889. function normalizeChartRows<T>(
  890. rows: unknown[],
  891. dateKey: string,
  892. numberKeys: string[]
  893. ): T[] {
  894. if (!Array.isArray(rows)) return [];
  895. return rows.map((r: unknown) => {
  896. const row = r as Record<string, unknown>;
  897. const out: Record<string, unknown> = {};
  898. out[dateKey] = row[dateKey] != null ? String(row[dateKey]) : "";
  899. numberKeys.forEach((k) => {
  900. out[k] = Number(row[k]) || 0;
  901. });
  902. return out as T;
  903. });
  904. }