FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

489 řádky
17 KiB

  1. "use server";
  2. import { revalidateTag } from "next/cache";
  3. import { BASE_API_URL } from "@/config/api";
  4. import { serverFetchJson } from "@/app/utils/fetchUtil";
  5. import type {
  6. LaneBtn,
  7. PostPickOrderResponse,
  8. ReleasedDoPickOrderListItem,
  9. StoreLaneSummary,
  10. } from "@/app/api/pickOrder/actions";
  11. import dayjs from "dayjs";
  12. /** Aligns with backend MessageResponse for workbench endpoints */
  13. export type WorkbenchMessageResponse = {
  14. id?: number | null;
  15. code?: string | null;
  16. name?: string | null;
  17. type?: string | null;
  18. message?: string | null;
  19. errorPosition?: string | null;
  20. entity?: unknown;
  21. };
  22. export async function startWorkbenchBatchReleaseAsync(data: {
  23. ids: number[];
  24. userId: number;
  25. }): Promise<WorkbenchMessageResponse> {
  26. const { ids, userId } = data;
  27. return serverFetchJson<WorkbenchMessageResponse>(
  28. `${BASE_API_URL}/doPickOrder/workbench/batch-release/async?userId=${userId}`,
  29. {
  30. method: "POST",
  31. body: JSON.stringify(ids),
  32. headers: { "Content-Type": "application/json" },
  33. }
  34. );
  35. }
  36. /** V2: no SPL/stock out at batch release; created when assigning delivery_order_pick_order. */
  37. export async function startWorkbenchBatchReleaseAsyncV2(data: {
  38. ids: number[];
  39. userId: number;
  40. mergeExtraIntoLaneTicket?: boolean;
  41. }): Promise<WorkbenchMessageResponse> {
  42. const { ids, userId, mergeExtraIntoLaneTicket = true } = data;
  43. return serverFetchJson<WorkbenchMessageResponse>(
  44. `${BASE_API_URL}/doPickOrder/workbench/batch-release/async-v2?userId=${userId}`,
  45. {
  46. method: "POST",
  47. body: JSON.stringify({ ids, mergeExtraIntoLaneTicket }),
  48. headers: { "Content-Type": "application/json" },
  49. }
  50. );
  51. }
  52. export async function workbenchBatchReleaseSyncV2(data: {
  53. ids: number[];
  54. userId: number;
  55. }): Promise<WorkbenchMessageResponse> {
  56. const { ids, userId } = data;
  57. return serverFetchJson<WorkbenchMessageResponse>(
  58. `${BASE_API_URL}/doPickOrder/workbench/batch-release/sync-v1?userId=${userId}`,
  59. {
  60. method: "POST",
  61. body: JSON.stringify(ids),
  62. headers: { "Content-Type": "application/json" },
  63. }
  64. );
  65. }
  66. export async function getWorkbenchBatchReleaseProgress(
  67. jobId: string
  68. ): Promise<WorkbenchMessageResponse> {
  69. return serverFetchJson<WorkbenchMessageResponse>(
  70. `${BASE_API_URL}/doPickOrder/workbench/batch-release/progress/${encodeURIComponent(jobId)}`,
  71. { method: "GET" }
  72. );
  73. }
  74. export type WorkbenchScanPickBody = {
  75. stockOutLineId: number;
  76. lotNo: string;
  77. /** From QR: ties to a single `inventory_lot` when lotNo is reused across stock-ins */
  78. stockInLineId?: number | null;
  79. /**
  80. * When set (e.g. label-print modal row), backend resolves this exact inventory lot line
  81. * instead of stockInLineId / newest-by-lotNo.
  82. */
  83. inventoryLotLineId?: number | null;
  84. /** Optional store scope (e.g. 2/F). When set, split re-suggestions must stay within the same store. */
  85. storeId?: string | null;
  86. /** Optional: exclude these warehouse codes from resuggest logic */
  87. excludeWarehouseCodes?: string[] | null;
  88. /** Optional decimal string or number serialized by JSON */
  89. qty?: number | string | null;
  90. /** Just Complete button (no QR) — DO user-audit flag */
  91. justComplete?: boolean | null;
  92. userId: number;
  93. };
  94. function serializeWorkbenchQty(
  95. qty: number | string | null | undefined
  96. ): number | undefined {
  97. if (qty === null || qty === undefined || qty === "") return undefined;
  98. const n = typeof qty === "string" ? Number(qty) : qty;
  99. if (typeof n !== "number" || Number.isNaN(n) || !Number.isFinite(n)) return undefined;
  100. // 0 is a valid explicit workbench short submit (must be sent, not omitted)
  101. return n;
  102. }
  103. /**
  104. * DO workbench scan-pick. Omit `qty` for full remaining on this SOL chunk (backend may split if lot runs out).
  105. * Pass `qty` less than remaining for short submit (POL/SOL completed without `partially_completed` on POL).
  106. * Pass `qty` greater than remaining to overscan: backend posts up to lot availability, then rebuild/ensure SOL.
  107. */
  108. export async function workbenchScanPick(
  109. body: WorkbenchScanPickBody
  110. ): Promise<WorkbenchMessageResponse> {
  111. const qty = serializeWorkbenchQty(body.qty);
  112. const sil = body.stockInLineId;
  113. const stockInLineId =
  114. typeof sil === "number" && Number.isFinite(sil) && sil > 0 ? sil : undefined;
  115. const storeId =
  116. typeof body.storeId === "string" && body.storeId.trim() !== ""
  117. ? body.storeId.trim()
  118. : undefined;
  119. const excludeWarehouseCodes =
  120. Array.isArray(body.excludeWarehouseCodes) && body.excludeWarehouseCodes.length > 0
  121. ? body.excludeWarehouseCodes
  122. .map((c) => (typeof c === "string" ? c.trim() : ""))
  123. .filter((c) => c !== "")
  124. : undefined;
  125. const ill = body.inventoryLotLineId;
  126. const inventoryLotLineId =
  127. typeof ill === "number" && Number.isFinite(ill) && ill > 0 ? ill : undefined;
  128. return serverFetchJson<WorkbenchMessageResponse>(
  129. `${BASE_API_URL}/doPickOrder/workbench/scan-pick`,
  130. {
  131. method: "POST",
  132. body: JSON.stringify({
  133. stockOutLineId: body.stockOutLineId,
  134. lotNo: body.lotNo,
  135. ...(stockInLineId !== undefined ? { stockInLineId } : {}),
  136. ...(inventoryLotLineId !== undefined ? { inventoryLotLineId } : {}),
  137. ...(storeId !== undefined ? { storeId } : {}),
  138. ...(excludeWarehouseCodes !== undefined ? { excludeWarehouseCodes } : {}),
  139. ...(qty !== undefined ? { qty } : {}),
  140. ...(body.justComplete === true ? { justComplete: true } : {}),
  141. userId: body.userId,
  142. }),
  143. headers: { "Content-Type": "application/json" },
  144. }
  145. );
  146. }
  147. export type WorkbenchBatchScanPickBody = {
  148. lines: WorkbenchScanPickBody[];
  149. };
  150. /**
  151. * DO workbench batch scan-pick.
  152. * Intended for batch-submit style flows where we close multiple SOLs (commonly qty=0 for noLot/expired/unavailable).
  153. */
  154. export async function workbenchBatchScanPick(
  155. body: WorkbenchBatchScanPickBody,
  156. ): Promise<WorkbenchMessageResponse> {
  157. const lines = Array.isArray(body.lines) ? body.lines : [];
  158. return serverFetchJson<WorkbenchMessageResponse>(
  159. `${BASE_API_URL}/doPickOrder/workbench/scan-pick/batch`,
  160. {
  161. method: "POST",
  162. body: JSON.stringify({
  163. lines: lines.map((l) => {
  164. const qty = serializeWorkbenchQty(l.qty);
  165. const sil = l.stockInLineId;
  166. const stockInLineId =
  167. typeof sil === "number" && Number.isFinite(sil) && sil > 0 ? sil : undefined;
  168. const storeId =
  169. typeof l.storeId === "string" && l.storeId.trim() !== "" ? l.storeId.trim() : undefined;
  170. const excludeWarehouseCodes =
  171. Array.isArray(l.excludeWarehouseCodes) && l.excludeWarehouseCodes.length > 0
  172. ? l.excludeWarehouseCodes
  173. .map((c) => (typeof c === "string" ? c.trim() : ""))
  174. .filter((c) => c !== "")
  175. : undefined;
  176. const ill = l.inventoryLotLineId;
  177. const inventoryLotLineId =
  178. typeof ill === "number" && Number.isFinite(ill) && ill > 0 ? ill : undefined;
  179. return {
  180. stockOutLineId: l.stockOutLineId,
  181. lotNo: l.lotNo ?? "",
  182. ...(stockInLineId !== undefined ? { stockInLineId } : {}),
  183. ...(inventoryLotLineId !== undefined ? { inventoryLotLineId } : {}),
  184. ...(storeId !== undefined ? { storeId } : {}),
  185. ...(excludeWarehouseCodes !== undefined ? { excludeWarehouseCodes } : {}),
  186. ...(qty !== undefined ? { qty } : {}),
  187. userId: l.userId,
  188. };
  189. }),
  190. }),
  191. headers: { "Content-Type": "application/json" },
  192. },
  193. );
  194. }
  195. /** Store lane grid backed by `delivery_order_pick_order` + `pick_order.deliveryOrderPickOrderId`. */
  196. export async function fetchWorkbenchStoreLaneSummary(
  197. storeId: string,
  198. requiredDate?: string,
  199. releaseType?: string
  200. ): Promise<StoreLaneSummary> {
  201. const dateToUse = requiredDate || dayjs().format("YYYY-MM-DD");
  202. const rt = releaseType || "all";
  203. const url = `${BASE_API_URL}/doPickOrder/workbench/summary-by-store?storeId=${encodeURIComponent(storeId)}&requiredDate=${encodeURIComponent(dateToUse)}&releaseType=${encodeURIComponent(rt)}`;
  204. return serverFetchJson<StoreLaneSummary>(url, {
  205. method: "GET",
  206. cache: "no-store",
  207. });
  208. }
  209. /** All Etra tickets (`releaseType=isExtra`) for a calendar day, grouped by shop → lanes. */
  210. export type WorkbenchEtraShopLaneGroup = {
  211. shopCode: string | null;
  212. shopName: string | null;
  213. lanes: LaneBtn[];
  214. };
  215. export async function fetchWorkbenchEtraLaneSummary(
  216. requiredDate?: string
  217. ): Promise<WorkbenchEtraShopLaneGroup[]> {
  218. const dateToUse = requiredDate || dayjs().format("YYYY-MM-DD");
  219. const url = `${BASE_API_URL}/doPickOrder/workbench/summary-is-etra?requiredDate=${encodeURIComponent(dateToUse)}`;
  220. const data = await serverFetchJson<WorkbenchEtraShopLaneGroup[]>(url, {
  221. method: "GET",
  222. cache: "no-store",
  223. });
  224. return Array.isArray(data) ? data : [];
  225. }
  226. /** Past-date `delivery_order_pick_order` tickets (same shape as `/doPickOrder/released`). */
  227. export async function fetchWorkbenchReleasedDoPickOrdersForSelection(
  228. shopName?: string,
  229. storeId?: string,
  230. truck?: string,
  231. releaseType?: string,
  232. /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */
  233. floor?: string
  234. ): Promise<ReleasedDoPickOrderListItem[]> {
  235. const params = new URLSearchParams();
  236. if (shopName?.trim()) params.append("shopName", shopName.trim());
  237. if (storeId?.trim()) params.append("storeId", storeId.trim());
  238. if (truck?.trim()) params.append("truck", truck.trim());
  239. if (releaseType?.trim()) params.append("releaseType", releaseType.trim());
  240. if (floor?.trim()) params.append("floor", floor.trim().replace("/", "").toUpperCase());
  241. const query = params.toString();
  242. const url = `${BASE_API_URL}/doPickOrder/workbench/released${query ? `?${query}` : ""}`;
  243. const response = await serverFetchJson<ReleasedDoPickOrderListItem[]>(url, { method: "GET" });
  244. return response ?? [];
  245. }
  246. /** When `requiredDeliveryDate` is set (YYYY-MM-DD), filters `delivery_order_pick_order.requiredDeliveryDate`; otherwise calendar today. */
  247. export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday(
  248. shopName?: string,
  249. storeId?: string,
  250. truck?: string,
  251. requiredDeliveryDate?: string,
  252. releaseType?: string,
  253. /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */
  254. floor?: string
  255. ): Promise<ReleasedDoPickOrderListItem[]> {
  256. const params = new URLSearchParams();
  257. if (shopName?.trim()) params.append("shopName", shopName.trim());
  258. if (storeId?.trim()) params.append("storeId", storeId.trim());
  259. if (truck?.trim()) params.append("truck", truck.trim());
  260. if (requiredDeliveryDate?.trim()) params.append("requiredDate", requiredDeliveryDate.trim());
  261. if (releaseType?.trim()) params.append("releaseType", releaseType.trim());
  262. if (floor?.trim()) params.append("floor", floor.trim().replace("/", "").toUpperCase());
  263. const query = params.toString();
  264. const url = `${BASE_API_URL}/doPickOrder/workbench/released-today${query ? `?${query}` : ""}`;
  265. const response = await serverFetchJson<ReleasedDoPickOrderListItem[]>(url, { method: "GET" });
  266. if (response == null) return [];
  267. return Array.isArray(response) ? response : [];
  268. }
  269. export type WorkbenchMergeTicketCandidate = {
  270. id: number;
  271. ticketNo: string | null;
  272. releaseType: string | null;
  273. shopId: number | null;
  274. shopCode: string | null;
  275. shopName: string | null;
  276. storeId: string | null;
  277. truckId: number | null;
  278. requiredDeliveryDate: string | null;
  279. truckLanceCode: string | null;
  280. truckDepartureTime: string | null;
  281. loadingSequence: number | null;
  282. deliveryOrderCodes: string[];
  283. laneKey: string;
  284. };
  285. export type WorkbenchMergeTicketCandidatesResponse = {
  286. batchFamilyTickets: WorkbenchMergeTicketCandidate[];
  287. isExtraTickets: WorkbenchMergeTicketCandidate[];
  288. };
  289. export async function fetchWorkbenchMergeTicketCandidates(data: {
  290. requiredDate: string;
  291. shopSearch?: string;
  292. }): Promise<WorkbenchMergeTicketCandidatesResponse> {
  293. const params = new URLSearchParams();
  294. params.set("requiredDate", data.requiredDate);
  295. if (data.shopSearch?.trim()) params.set("shopSearch", data.shopSearch.trim());
  296. const url = `${BASE_API_URL}/doPickOrder/workbench/merge-ticket-candidates?${params.toString()}`;
  297. const res = await serverFetchJson<WorkbenchMergeTicketCandidatesResponse>(url, {
  298. method: "GET",
  299. cache: "no-store",
  300. });
  301. return {
  302. batchFamilyTickets: res?.batchFamilyTickets ?? [],
  303. isExtraTickets: res?.isExtraTickets ?? [],
  304. };
  305. }
  306. export async function mergeWorkbenchTickets(data: {
  307. batchOrSingleDopoId: number;
  308. isExtraDopoId: number;
  309. }): Promise<WorkbenchMessageResponse> {
  310. return serverFetchJson<WorkbenchMessageResponse>(
  311. `${BASE_API_URL}/doPickOrder/workbench/merge-tickets`,
  312. {
  313. method: "POST",
  314. headers: { "Content-Type": "application/json" },
  315. body: JSON.stringify(data),
  316. },
  317. );
  318. }
  319. /** Same body as `/doPickOrder/assign-by-lane` but resolves `delivery_order_pick_order`. */
  320. export async function assignWorkbenchByLane(data: {
  321. userId: number;
  322. storeId: string;
  323. truckLanceCode: string;
  324. truckDepartureTime?: string;
  325. loadingSequence?: number | null;
  326. requiredDate?: string;
  327. /** Backend normalizes to isExtra / isExtra filter on `delivery_order_pick_order.releaseType` */
  328. releaseType?: string;
  329. }): Promise<PostPickOrderResponse> {
  330. const res = await serverFetchJson<PostPickOrderResponse>(
  331. `${BASE_API_URL}/doPickOrder/workbench/assign-by-lane`,
  332. {
  333. method: "POST",
  334. headers: { "Content-Type": "application/json" },
  335. body: JSON.stringify(data),
  336. }
  337. );
  338. revalidateTag("pickorder");
  339. return res;
  340. }
  341. /** Assign V1 (legacy): old FG-style, no atomic conflict guard. */
  342. export async function assignWorkbenchByLaneV1(data: {
  343. userId: number;
  344. storeId: string;
  345. truckLanceCode: string;
  346. truckDepartureTime?: string;
  347. requiredDate?: string;
  348. }): Promise<PostPickOrderResponse> {
  349. const res = await serverFetchJson<PostPickOrderResponse>(
  350. `${BASE_API_URL}/doPickOrder/workbench/assign-by-lane-v1`,
  351. {
  352. method: "POST",
  353. headers: { "Content-Type": "application/json" },
  354. body: JSON.stringify(data),
  355. }
  356. );
  357. revalidateTag("pickorder");
  358. return res;
  359. }
  360. export async function assignByDeliveryOrderPickOrderId(
  361. userId: number,
  362. deliveryOrderPickOrderId: number
  363. ): Promise<PostPickOrderResponse> {
  364. const res = await serverFetchJson<PostPickOrderResponse>(
  365. `${BASE_API_URL}/doPickOrder/workbench/assign-by-delivery-order-pick-order-id`,
  366. {
  367. method: "POST",
  368. headers: { "Content-Type": "application/json" },
  369. body: JSON.stringify({ userId, deliveryOrderPickOrderId }),
  370. }
  371. );
  372. revalidateTag("pickorder");
  373. return res;
  374. }
  375. /** Assign V1 (legacy): old FG-style, no atomic conflict guard. */
  376. export async function assignByDeliveryOrderPickOrderIdV1(
  377. userId: number,
  378. deliveryOrderPickOrderId: number
  379. ): Promise<PostPickOrderResponse> {
  380. const res = await serverFetchJson<PostPickOrderResponse>(
  381. `${BASE_API_URL}/doPickOrder/workbench/assign-by-delivery-order-pick-order-id-v1`,
  382. {
  383. method: "POST",
  384. headers: { "Content-Type": "application/json" },
  385. body: JSON.stringify({ userId, deliveryOrderPickOrderId }),
  386. }
  387. );
  388. revalidateTag("pickorder");
  389. return res;
  390. }
  391. export async function fetchWorkbenchCompletedLotDetails(
  392. deliveryOrderPickOrderId: number,
  393. ): Promise<any> {
  394. return serverFetchJson<any>(
  395. `${BASE_API_URL}/doPickOrder/workbench/completed-lot-details/${deliveryOrderPickOrderId}`,
  396. { method: "GET" },
  397. );
  398. }
  399. export type WorkbenchScanPayload = {
  400. itemId: number;
  401. stockInLineId: number;
  402. /** POL UomConversion id — when set, sameItemLots only include matching lot UOM */
  403. uomId?: number | null;
  404. };
  405. export async function fetchWorkbenchPrinters() {
  406. return serverFetchJson<any[]>(`${BASE_API_URL}/printers`, {
  407. method: "GET",
  408. cache: "no-store",
  409. });
  410. }
  411. export async function analyzeWorkbenchQrCode(payload: WorkbenchScanPayload) {
  412. return serverFetchJson<any>(`${BASE_API_URL}/inventoryLotLine/workbench/analyze-qr-code`, {
  413. method: "POST",
  414. headers: { "Content-Type": "application/json" },
  415. body: JSON.stringify(payload),
  416. cache: "no-store",
  417. });
  418. }
  419. export async function fetchWorkbenchAvailableLotsByItem(
  420. itemId: number,
  421. uomId?: number | null,
  422. ) {
  423. const qs =
  424. uomId != null && Number.isFinite(Number(uomId)) && Number(uomId) > 0
  425. ? `?uomId=${Number(uomId)}`
  426. : "";
  427. return serverFetchJson<any>(
  428. `${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}${qs}`,
  429. {
  430. method: "GET",
  431. cache: "no-store",
  432. },
  433. );
  434. }
  435. /** Single DO; JSON body is one number (same as legacy `batch-release/async-single`). */
  436. export async function startWorkbenchBatchReleaseAsyncSingleV2(data: {
  437. doId: number;
  438. userId: number;
  439. }): Promise<WorkbenchMessageResponse> {
  440. const { doId, userId } = data;
  441. return serverFetchJson<WorkbenchMessageResponse>(
  442. `${BASE_API_URL}/doPickOrder/workbench/batch-release/async-single-v2?userId=${userId}`,
  443. {
  444. method: "POST",
  445. body: JSON.stringify(doId),
  446. headers: { "Content-Type": "application/json" },
  447. }
  448. );
  449. }
  450. export async function printWorkbenchLotLabel(params: {
  451. inventoryLotLineId: number;
  452. printerId: number;
  453. printQty: number;
  454. }) {
  455. const searchParams = new URLSearchParams();
  456. searchParams.set("inventoryLotLineId", String(params.inventoryLotLineId));
  457. searchParams.set("printerId", String(params.printerId));
  458. searchParams.set("printQty", String(params.printQty));
  459. return serverFetchJson<WorkbenchMessageResponse>(
  460. `${BASE_API_URL}/inventoryLotLine/workbench/print-label?${searchParams.toString()}`,
  461. { method: "GET", cache: "no-store" },
  462. );
  463. }