"use server"; import { revalidateTag } from "next/cache"; import { BASE_API_URL } from "@/config/api"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import type { PostPickOrderResponse, ReleasedDoPickOrderListItem, StoreLaneSummary, } from "@/app/api/pickOrder/actions"; import dayjs from "dayjs"; /** Aligns with backend MessageResponse for workbench endpoints */ export type WorkbenchMessageResponse = { id?: number | null; code?: string | null; name?: string | null; type?: string | null; message?: string | null; errorPosition?: string | null; entity?: unknown; }; export async function startWorkbenchBatchReleaseAsync(data: { ids: number[]; userId: number; }): Promise { const { ids, userId } = data; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/batch-release/async?userId=${userId}`, { method: "POST", body: JSON.stringify(ids), headers: { "Content-Type": "application/json" }, } ); } /** V2: no SPL/stock out at batch release; created when assigning delivery_order_pick_order. */ export async function startWorkbenchBatchReleaseAsyncV2(data: { ids: number[]; userId: number; }): Promise { const { ids, userId } = data; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/batch-release/async-v2?userId=${userId}`, { method: "POST", body: JSON.stringify(ids), headers: { "Content-Type": "application/json" }, } ); } export async function workbenchBatchReleaseSyncV2(data: { ids: number[]; userId: number; }): Promise { const { ids, userId } = data; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/batch-release/sync-v1?userId=${userId}`, { method: "POST", body: JSON.stringify(ids), headers: { "Content-Type": "application/json" }, } ); } export async function getWorkbenchBatchReleaseProgress( jobId: string ): Promise { return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/batch-release/progress/${encodeURIComponent(jobId)}`, { method: "GET" } ); } export type WorkbenchScanPickBody = { stockOutLineId: number; lotNo: string; /** From QR: ties to a single `inventory_lot` when lotNo is reused across stock-ins */ stockInLineId?: number | null; /** * When set (e.g. label-print modal row), backend resolves this exact inventory lot line * instead of stockInLineId / newest-by-lotNo. */ inventoryLotLineId?: number | null; /** Optional store scope (e.g. 2/F). When set, split re-suggestions must stay within the same store. */ storeId?: string | null; /** Optional: exclude these warehouse codes from resuggest logic */ excludeWarehouseCodes?: string[] | null; /** Optional decimal string or number serialized by JSON */ qty?: number | string | null; userId: number; }; function serializeWorkbenchQty( qty: number | string | null | undefined ): number | undefined { if (qty === null || qty === undefined || qty === "") return undefined; const n = typeof qty === "string" ? Number(qty) : qty; if (typeof n !== "number" || Number.isNaN(n) || !Number.isFinite(n)) return undefined; // 0 is a valid explicit workbench short submit (must be sent, not omitted) return n; } /** * DO workbench scan-pick. Omit `qty` for full remaining on this SOL chunk (backend may split if lot runs out). * Pass `qty` less than remaining for short submit (POL/SOL completed without `partially_completed` on POL). * Pass `qty` greater than remaining to overscan: backend posts up to lot availability, then rebuild/ensure SOL. */ export async function workbenchScanPick( body: WorkbenchScanPickBody ): Promise { const qty = serializeWorkbenchQty(body.qty); const sil = body.stockInLineId; const stockInLineId = typeof sil === "number" && Number.isFinite(sil) && sil > 0 ? sil : undefined; const storeId = typeof body.storeId === "string" && body.storeId.trim() !== "" ? body.storeId.trim() : undefined; const excludeWarehouseCodes = Array.isArray(body.excludeWarehouseCodes) && body.excludeWarehouseCodes.length > 0 ? body.excludeWarehouseCodes .map((c) => (typeof c === "string" ? c.trim() : "")) .filter((c) => c !== "") : undefined; const ill = body.inventoryLotLineId; const inventoryLotLineId = typeof ill === "number" && Number.isFinite(ill) && ill > 0 ? ill : undefined; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/scan-pick`, { method: "POST", body: JSON.stringify({ stockOutLineId: body.stockOutLineId, lotNo: body.lotNo, ...(stockInLineId !== undefined ? { stockInLineId } : {}), ...(inventoryLotLineId !== undefined ? { inventoryLotLineId } : {}), ...(storeId !== undefined ? { storeId } : {}), ...(excludeWarehouseCodes !== undefined ? { excludeWarehouseCodes } : {}), ...(qty !== undefined ? { qty } : {}), userId: body.userId, }), headers: { "Content-Type": "application/json" }, } ); } export type WorkbenchBatchScanPickBody = { lines: WorkbenchScanPickBody[]; }; /** * DO workbench batch scan-pick. * Intended for batch-submit style flows where we close multiple SOLs (commonly qty=0 for noLot/expired/unavailable). */ export async function workbenchBatchScanPick( body: WorkbenchBatchScanPickBody, ): Promise { const lines = Array.isArray(body.lines) ? body.lines : []; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/scan-pick/batch`, { method: "POST", body: JSON.stringify({ lines: lines.map((l) => { const qty = serializeWorkbenchQty(l.qty); const sil = l.stockInLineId; const stockInLineId = typeof sil === "number" && Number.isFinite(sil) && sil > 0 ? sil : undefined; const storeId = typeof l.storeId === "string" && l.storeId.trim() !== "" ? l.storeId.trim() : undefined; const excludeWarehouseCodes = Array.isArray(l.excludeWarehouseCodes) && l.excludeWarehouseCodes.length > 0 ? l.excludeWarehouseCodes .map((c) => (typeof c === "string" ? c.trim() : "")) .filter((c) => c !== "") : undefined; const ill = l.inventoryLotLineId; const inventoryLotLineId = typeof ill === "number" && Number.isFinite(ill) && ill > 0 ? ill : undefined; return { stockOutLineId: l.stockOutLineId, lotNo: l.lotNo ?? "", ...(stockInLineId !== undefined ? { stockInLineId } : {}), ...(inventoryLotLineId !== undefined ? { inventoryLotLineId } : {}), ...(storeId !== undefined ? { storeId } : {}), ...(excludeWarehouseCodes !== undefined ? { excludeWarehouseCodes } : {}), ...(qty !== undefined ? { qty } : {}), userId: l.userId, }; }), }), headers: { "Content-Type": "application/json" }, }, ); } /** Store lane grid backed by `delivery_order_pick_order` + `pick_order.deliveryOrderPickOrderId`. */ export async function fetchWorkbenchStoreLaneSummary( storeId: string, requiredDate?: string, releaseType?: string ): Promise { const dateToUse = requiredDate || dayjs().format("YYYY-MM-DD"); const rt = releaseType || "all"; const url = `${BASE_API_URL}/doPickOrder/workbench/summary-by-store?storeId=${encodeURIComponent(storeId)}&requiredDate=${encodeURIComponent(dateToUse)}&releaseType=${encodeURIComponent(rt)}`; return serverFetchJson(url, { method: "GET", cache: "no-store", next: { revalidate: 0 }, }); } /** Past-date `delivery_order_pick_order` tickets (same shape as `/doPickOrder/released`). */ export async function fetchWorkbenchReleasedDoPickOrdersForSelection( shopName?: string, storeId?: string, truck?: string ): Promise { const params = new URLSearchParams(); if (shopName?.trim()) params.append("shopName", shopName.trim()); if (storeId?.trim()) params.append("storeId", storeId.trim()); if (truck?.trim()) params.append("truck", truck.trim()); const query = params.toString(); const url = `${BASE_API_URL}/doPickOrder/workbench/released${query ? `?${query}` : ""}`; const response = await serverFetchJson(url, { method: "GET" }); return response ?? []; } /** When `requiredDeliveryDate` is set (YYYY-MM-DD), filters `delivery_order_pick_order.requiredDeliveryDate`; otherwise calendar today. */ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday( shopName?: string, storeId?: string, truck?: string, requiredDeliveryDate?: string ): Promise { const params = new URLSearchParams(); if (shopName?.trim()) params.append("shopName", shopName.trim()); if (storeId?.trim()) params.append("storeId", storeId.trim()); if (truck?.trim()) params.append("truck", truck.trim()); if (requiredDeliveryDate?.trim()) params.append("requiredDate", requiredDeliveryDate.trim()); const query = params.toString(); const url = `${BASE_API_URL}/doPickOrder/workbench/released-today${query ? `?${query}` : ""}`; const response = await serverFetchJson(url, { method: "GET" }); if (response == null) return []; return Array.isArray(response) ? response : []; } /** Same body as `/doPickOrder/assign-by-lane` but resolves `delivery_order_pick_order`. */ export async function assignWorkbenchByLane(data: { userId: number; storeId: string; truckLanceCode: string; truckDepartureTime?: string; loadingSequence?: number | null; requiredDate?: string; }): Promise { const res = await serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/assign-by-lane`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), } ); revalidateTag("pickorder"); return res; } /** Assign V1 (legacy): old FG-style, no atomic conflict guard. */ export async function assignWorkbenchByLaneV1(data: { userId: number; storeId: string; truckLanceCode: string; truckDepartureTime?: string; requiredDate?: string; }): Promise { const res = await serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/assign-by-lane-v1`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), } ); revalidateTag("pickorder"); return res; } export async function assignByDeliveryOrderPickOrderId( userId: number, deliveryOrderPickOrderId: number ): Promise { const res = await serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/assign-by-delivery-order-pick-order-id`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, deliveryOrderPickOrderId }), } ); revalidateTag("pickorder"); return res; } /** Assign V1 (legacy): old FG-style, no atomic conflict guard. */ export async function assignByDeliveryOrderPickOrderIdV1( userId: number, deliveryOrderPickOrderId: number ): Promise { const res = await serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/assign-by-delivery-order-pick-order-id-v1`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, deliveryOrderPickOrderId }), } ); revalidateTag("pickorder"); return res; } export async function fetchWorkbenchCompletedLotDetails( deliveryOrderPickOrderId: number, ): Promise { return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/completed-lot-details/${deliveryOrderPickOrderId}`, { method: "GET" }, ); } export type WorkbenchScanPayload = { itemId: number; stockInLineId: number; }; export async function fetchWorkbenchPrinters() { return serverFetchJson(`${BASE_API_URL}/printers`, { method: "GET", cache: "no-store", }); } export async function analyzeWorkbenchQrCode(payload: WorkbenchScanPayload) { return serverFetchJson(`${BASE_API_URL}/inventoryLotLine/workbench/analyze-qr-code`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), cache: "no-store", }); } export async function fetchWorkbenchAvailableLotsByItem(itemId: number) { return serverFetchJson( `${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}`, { method: "GET", cache: "no-store", }, ); } /** Single DO; JSON body is one number (same as legacy `batch-release/async-single`). */ export async function startWorkbenchBatchReleaseAsyncSingleV2(data: { doId: number; userId: number; }): Promise { const { doId, userId } = data; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/batch-release/async-single-v2?userId=${userId}`, { method: "POST", body: JSON.stringify(doId), headers: { "Content-Type": "application/json" }, } ); } export async function printWorkbenchLotLabel(params: { inventoryLotLineId: number; printerId: number; printQty: number; }) { const searchParams = new URLSearchParams(); searchParams.set("inventoryLotLineId", String(params.inventoryLotLineId)); searchParams.set("printerId", String(params.printerId)); searchParams.set("printQty", String(params.printQty)); return serverFetchJson( `${BASE_API_URL}/inventoryLotLine/workbench/print-label?${searchParams.toString()}`, { method: "GET", cache: "no-store" }, ); }