"use server"; import { revalidateTag } from "next/cache"; import { BASE_API_URL } from "@/config/api"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import type { LaneBtn, 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; mergeExtraIntoLaneTicket?: boolean; }): Promise { const { ids, userId, mergeExtraIntoLaneTicket = true } = data; return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/batch-release/async-v2?userId=${userId}`, { method: "POST", body: JSON.stringify({ ids, mergeExtraIntoLaneTicket }), 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; /** Just Complete button (no QR) — DO user-audit flag */ justComplete?: boolean | 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 } : {}), ...(body.justComplete === true ? { justComplete: true } : {}), 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", }); } /** All Etra tickets (`releaseType=isExtra`) for a calendar day, grouped by shop → lanes. */ export type WorkbenchEtraShopLaneGroup = { shopCode: string | null; shopName: string | null; lanes: LaneBtn[]; }; export async function fetchWorkbenchEtraLaneSummary( requiredDate?: string ): Promise { const dateToUse = requiredDate || dayjs().format("YYYY-MM-DD"); const url = `${BASE_API_URL}/doPickOrder/workbench/summary-is-etra?requiredDate=${encodeURIComponent(dateToUse)}`; const data = await serverFetchJson(url, { method: "GET", cache: "no-store", }); return Array.isArray(data) ? data : []; } /** Past-date `delivery_order_pick_order` tickets (same shape as `/doPickOrder/released`). */ export async function fetchWorkbenchReleasedDoPickOrdersForSelection( shopName?: string, storeId?: string, truck?: string, releaseType?: string, /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */ floor?: 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 (releaseType?.trim()) params.append("releaseType", releaseType.trim()); if (floor?.trim()) params.append("floor", floor.trim().replace("/", "").toUpperCase()); 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, releaseType?: string, /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */ floor?: 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()); if (releaseType?.trim()) params.append("releaseType", releaseType.trim()); if (floor?.trim()) params.append("floor", floor.trim().replace("/", "").toUpperCase()); 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 : []; } export type WorkbenchMergeTicketCandidate = { id: number; ticketNo: string | null; releaseType: string | null; shopId: number | null; shopCode: string | null; shopName: string | null; storeId: string | null; truckId: number | null; requiredDeliveryDate: string | null; truckLanceCode: string | null; truckDepartureTime: string | null; loadingSequence: number | null; deliveryOrderCodes: string[]; laneKey: string; }; export type WorkbenchMergeTicketCandidatesResponse = { batchFamilyTickets: WorkbenchMergeTicketCandidate[]; isExtraTickets: WorkbenchMergeTicketCandidate[]; }; export async function fetchWorkbenchMergeTicketCandidates(data: { requiredDate: string; shopSearch?: string; }): Promise { const params = new URLSearchParams(); params.set("requiredDate", data.requiredDate); if (data.shopSearch?.trim()) params.set("shopSearch", data.shopSearch.trim()); const url = `${BASE_API_URL}/doPickOrder/workbench/merge-ticket-candidates?${params.toString()}`; const res = await serverFetchJson(url, { method: "GET", cache: "no-store", }); return { batchFamilyTickets: res?.batchFamilyTickets ?? [], isExtraTickets: res?.isExtraTickets ?? [], }; } export async function mergeWorkbenchTickets(data: { batchOrSingleDopoId: number; isExtraDopoId: number; }): Promise { return serverFetchJson( `${BASE_API_URL}/doPickOrder/workbench/merge-tickets`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }, ); } /** 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; /** Backend normalizes to isExtra / isExtra filter on `delivery_order_pick_order.releaseType` */ releaseType?: 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; /** POL UomConversion id — when set, sameItemLots only include matching lot UOM */ uomId?: number | null; }; 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, uomId?: number | null, ) { const qs = uomId != null && Number.isFinite(Number(uomId)) && Number(uomId) > 0 ? `?uomId=${Number(uomId)}` : ""; return serverFetchJson( `${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}${qs}`, { 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" }, ); }