"use server"; // import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; // import { BASE_API_URL } from "@/config/api"; import { serverFetch, serverFetchJson, serverFetchWithNoContent, ServerFetchError, } from "../../utils/fetchUtil"; import { BASE_API_URL } from "../../../config/api"; import { revalidateTag } from "next/cache"; import { cache } from "react"; export interface ShopAndTruck{ id: number; name: String; code: String; addr1: String; addr2: String; addr3: String; contactNo: number; type: String; contactEmail: String; contactName: String; truckLanceCode: String; DepartureTime: String; LoadingSequence?: number | null; districtReference: string | null; Store_id: Number; remark?: String | null; truckId?: number; } export interface Shop{ id: number; name: String; code: String; addr3: String; } export interface Truck{ id?: number; truckLanceCode: String; departureTime: String | number[]; loadingSequence: number; districtReference: string | null; storeId: Number | String; remark?: String | null; shopName?: String | null; shopCode?: String | null; } export interface SaveTruckLane { id: number; truckLanceCode: string; departureTime: string; loadingSequence: number; districtReference: string | null; storeId: string; remark?: string | null; logisticId?: number | null; /** When true, set truck.logistic to logisticId (null clears). When false/omit, do not change logistic. */ updateLogistic?: boolean; } /** POST /truck/updateLaneLogistic — 同線桶內 truck 列一次更新 logistic(單一 transaction) */ export interface UpdateLaneLogisticRequest { truckLanceCode: string; remark?: string | null; logisticId?: number | null; } export interface DeleteTruckLane { id: number; } export interface UpdateTruckShopDetailsRequest { id: number; shopId?: number | null; shopName: string | null; shopCode: string | null; loadingSequence: number; remark?: string | null; } export interface SaveTruckRequest { id?: number | null; store_id: string; truckLanceCode: string; departureTime: string; shopId: number; shopName: string; shopCode: string; loadingSequence: number; districtReference?: string | null; remark?: string | null; logisticId?: number | null; } export interface CreateTruckWithoutShopRequest { store_id: string; truckLanceCode: string; departureTime: string; loadingSequence?: number; districtReference?: string | null; logisticId?: number | null; remark?: string | null; } export interface MessageResponse { id: number | null; name: string | null; code: string | null; type: string; message: string; errorPosition: string | null; entity: Truck | null; } export const fetchAllShopsAction = cache(async (params?: Record) => { const endpoint = `${BASE_API_URL}/shop/combo/allShop`; const qs = params ? Object.entries(params) .filter(([, v]) => v !== null && v !== undefined && String(v).trim() !== "") .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) .join("&") : ""; const url = qs ? `${endpoint}?${qs}` : endpoint; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findTruckLaneByShopIdAction = cache(async (shopId: number | string) => { const endpoint = `${BASE_API_URL}/truck/findTruckLane/${shopId}`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const updateTruckLaneAction = async (data: SaveTruckLane) => { const endpoint = `${BASE_API_URL}/truck/updateTruckLane`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; export const updateLaneLogisticAction = async ( data: UpdateLaneLogisticRequest, ): Promise => { const endpoint = `${BASE_API_URL}/truck/updateLaneLogistic`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; export const deleteTruckLaneAction = async (data: DeleteTruckLane) => { const endpoint = `${BASE_API_URL}/truck/deleteTruckLane`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; export const createTruckAction = async (data: SaveTruckRequest) => { const endpoint = `${BASE_API_URL}/truck/createTruckInShop`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; export const findAllUniqueTruckLaneCombinationsAction = cache(async () => { const endpoint = `${BASE_API_URL}/truck/findAllUniqueTruckLanceCodeAndRemarkCombinations`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); /** O(1) 取整個 RouteBoard 所需 truck rows;前端自行按 (truckLanceCode, remark) 分桶。 */ export const findAllForRouteBoardAction = cache(async () => { const endpoint = `${BASE_API_URL}/truck/findAllForRouteBoard`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLanceCode: string, remark: string) => { const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndRemarkAndDeletedFalse`; const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}&remark=${encodeURIComponent(remark)}`; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: string) => { const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse`; const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findAllByTruckLanceCodeAndDeletedFalseAction = cache(async (truckLanceCode: string) => { const endpoint = `${BASE_API_URL}/truck/findAllByTruckLanceCodeAndDeletedFalse`; const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); /** 與 `findAllUniqueTruckLanceCodeAndRemarkCombinations` 同一 (code, remark) 桶;remark 空則不帶參數。 */ export const findAllByTruckLanceCodeAndRemarkAndDeletedFalseAction = cache( async (truckLanceCode: string, remark: string | null | undefined) => { const endpoint = `${BASE_API_URL}/truck/findAllByTruckLanceCodeAndRemarkAndDeletedFalse`; const params = new URLSearchParams(); params.set("truckLanceCode", truckLanceCode); const r = remark != null && String(remark).trim() !== "" ? String(remark).trim() : ""; if (r !== "") params.set("remark", r); const url = `${endpoint}?${params.toString()}`; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }, ); export const updateTruckShopDetailsAction = async (data: UpdateTruckShopDetailsRequest) => { const endpoint = `${BASE_API_URL}/truck/updateTruckShopDetails`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; export const createTruckWithoutShopAction = async (data: CreateTruckWithoutShopRequest) => { const endpoint = `${BASE_API_URL}/truck/createTruckWithoutShop`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; /** PDF 圖1:每車線一個 worksheet(MTMS_ROUTE_V1)。回傳 base64 方便 client 下載。 */ export const exportRouteLanesExcelAction = async ( laneIds: string[], ): Promise<{ base64: string; filename: string }> => { const response = await serverFetch(`${BASE_API_URL}/truck/exportRouteLanesExcel`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, body: JSON.stringify({ laneIds }), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new ServerFetchError( `Export failed: ${response.status} ${text}`.trim(), response, ); } const cd = response.headers.get("content-disposition") ?? ""; let filename = `MTMS_車線_${Date.now()}.xlsx`; const quoted = /filename="([^"]+)"/i.exec(cd); const star = /filename\*=UTF-8''([^;\s]+)/i.exec(cd); const raw = (star?.[1] || quoted?.[1])?.trim(); if (raw) { try { filename = decodeURIComponent(raw); } catch { filename = raw; } } const buf = await response.arrayBuffer(); return { base64: Buffer.from(buf).toString("base64"), filename, }; }; /** 圖2:車線 Report(單一 sheet;每間物流公司一個水平區塊)。 */ export const exportRouteReportExcelAction = async ( laneIds: string[], ): Promise<{ base64: string; filename: string }> => { const response = await serverFetch(`${BASE_API_URL}/truck/exportRouteReportExcel`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, body: JSON.stringify({ laneIds }), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new ServerFetchError( `Export failed: ${response.status} ${text}`.trim(), response, ); } const cd = response.headers.get("content-disposition") ?? ""; let filename = `車線Report_${Date.now()}.xlsx`; const quoted = /filename="([^"]+)"/i.exec(cd); const star = /filename\*=UTF-8''([^;\s]+)/i.exec(cd); const raw = (star?.[1] || quoted?.[1])?.trim(); if (raw) { try { filename = decodeURIComponent(raw); } catch { filename = raw; } } const buf = await response.arrayBuffer(); return { base64: Buffer.from(buf).toString("base64"), filename, }; }; export const exportTruckLaneVersionReportExcelAction = async ( fromVersionId: number, toVersionId: number, ): Promise<{ base64: string; filename: string }> => { const response = await serverFetch( `${BASE_API_URL}/truck/exportTruckLaneVersionReportExcel`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, body: JSON.stringify({ fromVersionId, toVersionId }), }, ); if (!response.ok) { const text = await response.text().catch(() => ""); throw new ServerFetchError( `Export failed: ${response.status} ${text}`.trim(), response, ); } const cd = response.headers.get("content-disposition") ?? ""; let filename = `車線版本報告_${Date.now()}.xlsx`; const quoted = /filename="([^"]+)"/i.exec(cd); const star = /filename\*=UTF-8''([^;\s]+)/i.exec(cd); const raw = (star?.[1] || quoted?.[1])?.trim(); if (raw) { try { filename = decodeURIComponent(raw); } catch { filename = raw; } } const buf = await response.arrayBuffer(); return { base64: Buffer.from(buf).toString("base64"), filename, }; }; export const importRouteLanesExcelAction = async ( formData: FormData, ): Promise => { const response = await serverFetch(`${BASE_API_URL}/truck/importRouteLanesExcel`, { method: "POST", body: formData, }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new ServerFetchError( `Import failed: ${response.status} ${text}`.trim(), response, ); } return (await response.json()) as MessageResponse; }; export type RouteLaneImportPreviewRow = { truckRowId: number | null; truckLanceCode: string; remark: string | null; storeId: string; departureTime: string; shopId: number; shopName: string; shopCode: string; loadingSequence: number; districtReference: string | null; logisticId: number | null; }; export type ParseRouteLanesExcelResponse = { sheetCount: number; rowCount: number; rows: RouteLaneImportPreviewRow[]; }; export const parseRouteLanesExcelAction = async ( formData: FormData, ): Promise => { const response = await serverFetch(`${BASE_API_URL}/truck/parseRouteLanesExcel`, { method: "POST", body: formData, }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new ServerFetchError( `Parse import failed: ${response.status} ${text}`.trim(), response, ); } return (await response.json()) as ParseRouteLanesExcelResponse; }; export const findAllUniqueShopNamesAndCodesFromTrucksAction = cache(async () => { const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesAndCodesFromTrucks`; return serverFetchJson>(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findAllUniqueRemarksFromTrucksAction = cache(async () => { const endpoint = `${BASE_API_URL}/truck/findAllUniqueRemarksFromTrucks`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findAllUniqueShopCodesFromTrucksAction = cache(async () => { const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopCodesFromTrucks`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const findAllUniqueShopNamesFromTrucksAction = cache(async () => { const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesFromTrucks`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); // ---- Truck lane version snapshot (DB snapshot) ---- export interface CreateTruckLaneSnapshotRequest { truckLanceCode?: string | null; note?: string | null; } export interface TruckLaneVersionResponse { id: number; truckLanceCode: string; note: string | null; created: string | null; /** truck_lane_version.modifiedBy(BaseEntity) */ modifiedBy?: string | null; } export interface TruckLaneVersionLineResponse { truckRowId: number; truckLanceCode: string | null; shopCode: string | null; branchName: string | null; districtReference: string | null; loadingSequence: number | null; departureTime: string | null; storeId: string; remark: string | null; logisticId: number | null; } export type DiffFieldChange = { field: string; from: string | null; to: string | null; }; export type TruckLaneVersionDiffLine = { truckRowId: number; shopCode: string | null; changes: DiffFieldChange[]; }; export type LogisticMasterDiffLine = { logisticId: number; type: string; logisticName: string; carPlate: string; changeText: string; }; export type TruckLaneVersionDiffResponse = { fromVersionId: number; toVersionId: number; changed: TruckLaneVersionDiffLine[]; logisticMasterChanges?: LogisticMasterDiffLine[]; }; export const createTruckLaneSnapshotAction = async (data: CreateTruckLaneSnapshotRequest) => { const endpoint = `${BASE_API_URL}/truckLaneVersion/snapshot`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; export const listTruckLaneVersionsAction = cache(async (truckLanceCode?: string | null) => { const endpoint = `${BASE_API_URL}/truckLaneVersion`; const url = truckLanceCode != null && String(truckLanceCode).trim() !== "" ? `${endpoint}?truckLanceCode=${encodeURIComponent(String(truckLanceCode))}` : endpoint; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const getTruckLaneVersionLinesAction = cache(async (versionId: number) => { const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/lines`; return serverFetchJson(endpoint, { method: "GET", headers: { "Content-Type": "application/json" }, }); }); export const diffTruckLaneVersionsAction = async ( fromVersionId: number, toVersionId: number, ) => { const endpoint = `${BASE_API_URL}/truckLaneVersion/diff`; const url = `${endpoint}?fromVersionId=${encodeURIComponent(String(fromVersionId))}&toVersionId=${encodeURIComponent(String(toVersionId))}`; return serverFetchJson(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); }; export const restoreTruckLaneVersionAction = async (versionId: number) => { const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/restore`; return serverFetchJson(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, }); }; export type UpdateTruckLaneVersionNoteRequest = { note: string | null; }; export const updateTruckLaneVersionNoteAction = async ( versionId: number, data: UpdateTruckLaneVersionNoteRequest, ) => { const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/note`; return serverFetchJson(endpoint, { method: "PATCH", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); };