FPSMS-frontend
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 

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