FPSMS-frontend
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

602 satır
18 KiB

  1. "use server";
  2. // import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
  3. // import { BASE_API_URL } from "@/config/api";
  4. import {
  5. serverFetch,
  6. serverFetchJson,
  7. serverFetchWithNoContent,
  8. ServerFetchError,
  9. } from "../../utils/fetchUtil";
  10. import { BASE_API_URL } from "../../../config/api";
  11. import { revalidateTag } from "next/cache";
  12. import { cache } from "react";
  13. export interface ShopAndTruck{
  14. id: number;
  15. name: String;
  16. code: String;
  17. addr1: String;
  18. addr2: String;
  19. addr3: String;
  20. contactNo: number;
  21. type: String;
  22. contactEmail: String;
  23. contactName: String;
  24. truckLanceCode: String;
  25. DepartureTime: String;
  26. LoadingSequence?: number | null;
  27. districtReference: string | null;
  28. Store_id: Number;
  29. remark?: String | null;
  30. truckId?: number;
  31. }
  32. export interface Shop{
  33. id: number;
  34. name: String;
  35. code: String;
  36. addr3: String;
  37. }
  38. export interface Truck{
  39. id?: number;
  40. truckLanceCode: String;
  41. departureTime: String | number[];
  42. loadingSequence: number;
  43. districtReference: string | null;
  44. storeId: Number | String;
  45. remark?: String | null;
  46. shopName?: String | null;
  47. shopCode?: String | null;
  48. }
  49. export interface SaveTruckLane {
  50. id: number;
  51. truckLanceCode: string;
  52. departureTime: string;
  53. loadingSequence: number;
  54. districtReference: string | null;
  55. storeId: string;
  56. remark?: string | null;
  57. logisticId?: number | null;
  58. /** When true, set truck.logistic to logisticId (null clears). When false/omit, do not change logistic. */
  59. updateLogistic?: boolean;
  60. }
  61. /** POST /truck/updateLaneLogistic — 同線桶內 truck 列一次更新 logistic(單一 transaction) */
  62. export interface UpdateLaneLogisticRequest {
  63. truckLanceCode: string;
  64. remark?: string | null;
  65. logisticId?: number | null;
  66. }
  67. export interface DeleteTruckLane {
  68. id: number;
  69. }
  70. export interface UpdateTruckShopDetailsRequest {
  71. id: number;
  72. shopId?: number | null;
  73. shopName: string | null;
  74. shopCode: string | null;
  75. loadingSequence: number;
  76. remark?: string | null;
  77. }
  78. export interface SaveTruckRequest {
  79. id?: number | null;
  80. store_id: string;
  81. truckLanceCode: string;
  82. departureTime: string;
  83. shopId: number;
  84. shopName: string;
  85. shopCode: string;
  86. loadingSequence: number;
  87. districtReference?: string | null;
  88. remark?: string | null;
  89. logisticId?: number | null;
  90. }
  91. export interface CreateTruckWithoutShopRequest {
  92. store_id: string;
  93. truckLanceCode: string;
  94. departureTime: string;
  95. loadingSequence?: number;
  96. districtReference?: string | null;
  97. logisticId?: number | null;
  98. remark?: string | null;
  99. }
  100. export interface MessageResponse {
  101. id: number | null;
  102. name: string | null;
  103. code: string | null;
  104. type: string;
  105. message: string;
  106. errorPosition: string | null;
  107. entity: Truck | null;
  108. }
  109. export const fetchAllShopsAction = cache(async (params?: Record<string, string | number | null>) => {
  110. const endpoint = `${BASE_API_URL}/shop/combo/allShop`;
  111. const qs = params
  112. ? Object.entries(params)
  113. .filter(([, v]) => v !== null && v !== undefined && String(v).trim() !== "")
  114. .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
  115. .join("&")
  116. : "";
  117. const url = qs ? `${endpoint}?${qs}` : endpoint;
  118. return serverFetchJson<ShopAndTruck[]>(url, {
  119. method: "GET",
  120. headers: { "Content-Type": "application/json" },
  121. });
  122. });
  123. export const findTruckLaneByShopIdAction = cache(async (shopId: number | string) => {
  124. const endpoint = `${BASE_API_URL}/truck/findTruckLane/${shopId}`;
  125. return serverFetchJson<Truck[]>(endpoint, {
  126. method: "GET",
  127. headers: { "Content-Type": "application/json" },
  128. });
  129. });
  130. export const updateTruckLaneAction = async (data: SaveTruckLane) => {
  131. const endpoint = `${BASE_API_URL}/truck/updateTruckLane`;
  132. return serverFetchJson<MessageResponse>(endpoint, {
  133. method: "POST",
  134. body: JSON.stringify(data),
  135. headers: { "Content-Type": "application/json" },
  136. });
  137. };
  138. export const updateLaneLogisticAction = async (
  139. data: UpdateLaneLogisticRequest,
  140. ): Promise<MessageResponse> => {
  141. const endpoint = `${BASE_API_URL}/truck/updateLaneLogistic`;
  142. return serverFetchJson<MessageResponse>(endpoint, {
  143. method: "POST",
  144. body: JSON.stringify(data),
  145. headers: { "Content-Type": "application/json" },
  146. });
  147. };
  148. export const deleteTruckLaneAction = async (data: DeleteTruckLane) => {
  149. const endpoint = `${BASE_API_URL}/truck/deleteTruckLane`;
  150. return serverFetchJson<MessageResponse>(endpoint, {
  151. method: "POST",
  152. body: JSON.stringify(data),
  153. headers: { "Content-Type": "application/json" },
  154. });
  155. };
  156. export const createTruckAction = async (data: SaveTruckRequest) => {
  157. const endpoint = `${BASE_API_URL}/truck/createTruckInShop`;
  158. return serverFetchJson<MessageResponse>(endpoint, {
  159. method: "POST",
  160. body: JSON.stringify(data),
  161. headers: { "Content-Type": "application/json" },
  162. });
  163. };
  164. export const findAllUniqueTruckLaneCombinationsAction = cache(async () => {
  165. const endpoint = `${BASE_API_URL}/truck/findAllUniqueTruckLanceCodeAndRemarkCombinations`;
  166. return serverFetchJson<Truck[]>(endpoint, {
  167. method: "GET",
  168. headers: { "Content-Type": "application/json" },
  169. });
  170. });
  171. /** O(1) 取整個 RouteBoard 所需 truck rows;前端自行按 (truckLanceCode, remark) 分桶。 */
  172. export const findAllForRouteBoardAction = cache(async () => {
  173. const endpoint = `${BASE_API_URL}/truck/findAllForRouteBoard`;
  174. return serverFetchJson<Truck[]>(endpoint, {
  175. method: "GET",
  176. headers: { "Content-Type": "application/json" },
  177. });
  178. });
  179. export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLanceCode: string, remark: string) => {
  180. const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndRemarkAndDeletedFalse`;
  181. const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}&remark=${encodeURIComponent(remark)}`;
  182. return serverFetchJson<ShopAndTruck[]>(url, {
  183. method: "GET",
  184. headers: { "Content-Type": "application/json" },
  185. });
  186. });
  187. export const findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: string) => {
  188. const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse`;
  189. const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`;
  190. return serverFetchJson<ShopAndTruck[]>(url, {
  191. method: "GET",
  192. headers: { "Content-Type": "application/json" },
  193. });
  194. });
  195. export const findAllByTruckLanceCodeAndDeletedFalseAction = cache(async (truckLanceCode: string) => {
  196. const endpoint = `${BASE_API_URL}/truck/findAllByTruckLanceCodeAndDeletedFalse`;
  197. const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`;
  198. return serverFetchJson<Truck[]>(url, {
  199. method: "GET",
  200. headers: { "Content-Type": "application/json" },
  201. });
  202. });
  203. /** 與 `findAllUniqueTruckLanceCodeAndRemarkCombinations` 同一 (code, remark) 桶;remark 空則不帶參數。 */
  204. export const findAllByTruckLanceCodeAndRemarkAndDeletedFalseAction = cache(
  205. async (truckLanceCode: string, remark: string | null | undefined) => {
  206. const endpoint = `${BASE_API_URL}/truck/findAllByTruckLanceCodeAndRemarkAndDeletedFalse`;
  207. const params = new URLSearchParams();
  208. params.set("truckLanceCode", truckLanceCode);
  209. const r = remark != null && String(remark).trim() !== "" ? String(remark).trim() : "";
  210. if (r !== "") params.set("remark", r);
  211. const url = `${endpoint}?${params.toString()}`;
  212. return serverFetchJson<Truck[]>(url, {
  213. method: "GET",
  214. headers: { "Content-Type": "application/json" },
  215. });
  216. },
  217. );
  218. export const updateTruckShopDetailsAction = async (data: UpdateTruckShopDetailsRequest) => {
  219. const endpoint = `${BASE_API_URL}/truck/updateTruckShopDetails`;
  220. return serverFetchJson<MessageResponse>(endpoint, {
  221. method: "POST",
  222. body: JSON.stringify(data),
  223. headers: { "Content-Type": "application/json" },
  224. });
  225. };
  226. export const createTruckWithoutShopAction = async (data: CreateTruckWithoutShopRequest) => {
  227. const endpoint = `${BASE_API_URL}/truck/createTruckWithoutShop`;
  228. return serverFetchJson<MessageResponse>(endpoint, {
  229. method: "POST",
  230. body: JSON.stringify(data),
  231. headers: { "Content-Type": "application/json" },
  232. });
  233. };
  234. /** PDF 圖1:每車線一個 worksheet(MTMS_ROUTE_V1)。回傳 base64 方便 client 下載。 */
  235. export const exportRouteLanesExcelAction = async (
  236. laneIds: string[],
  237. ): Promise<{ base64: string; filename: string }> => {
  238. const response = await serverFetch(`${BASE_API_URL}/truck/exportRouteLanesExcel`, {
  239. method: "POST",
  240. headers: {
  241. "Content-Type": "application/json",
  242. Accept:
  243. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  244. },
  245. body: JSON.stringify({ laneIds }),
  246. });
  247. if (!response.ok) {
  248. const text = await response.text().catch(() => "");
  249. throw new ServerFetchError(
  250. `Export failed: ${response.status} ${text}`.trim(),
  251. response,
  252. );
  253. }
  254. const cd = response.headers.get("content-disposition") ?? "";
  255. let filename = `MTMS_車線_${Date.now()}.xlsx`;
  256. const quoted = /filename="([^"]+)"/i.exec(cd);
  257. const star = /filename\*=UTF-8''([^;\s]+)/i.exec(cd);
  258. const raw = (star?.[1] || quoted?.[1])?.trim();
  259. if (raw) {
  260. try {
  261. filename = decodeURIComponent(raw);
  262. } catch {
  263. filename = raw;
  264. }
  265. }
  266. const buf = await response.arrayBuffer();
  267. return {
  268. base64: Buffer.from(buf).toString("base64"),
  269. filename,
  270. };
  271. };
  272. /** 圖2:車線 Report(單一 sheet;每間物流公司一個水平區塊)。 */
  273. export const exportRouteReportExcelAction = async (
  274. laneIds: string[],
  275. ): Promise<{ base64: string; filename: string }> => {
  276. const response = await serverFetch(`${BASE_API_URL}/truck/exportRouteReportExcel`, {
  277. method: "POST",
  278. headers: {
  279. "Content-Type": "application/json",
  280. Accept:
  281. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  282. },
  283. body: JSON.stringify({ laneIds }),
  284. });
  285. if (!response.ok) {
  286. const text = await response.text().catch(() => "");
  287. throw new ServerFetchError(
  288. `Export failed: ${response.status} ${text}`.trim(),
  289. response,
  290. );
  291. }
  292. const cd = response.headers.get("content-disposition") ?? "";
  293. let filename = `車線Report_${Date.now()}.xlsx`;
  294. const quoted = /filename="([^"]+)"/i.exec(cd);
  295. const star = /filename\*=UTF-8''([^;\s]+)/i.exec(cd);
  296. const raw = (star?.[1] || quoted?.[1])?.trim();
  297. if (raw) {
  298. try {
  299. filename = decodeURIComponent(raw);
  300. } catch {
  301. filename = raw;
  302. }
  303. }
  304. const buf = await response.arrayBuffer();
  305. return {
  306. base64: Buffer.from(buf).toString("base64"),
  307. filename,
  308. };
  309. };
  310. export const exportTruckLaneVersionReportExcelAction = async (
  311. fromVersionId: number,
  312. toVersionId: number,
  313. ): Promise<{ base64: string; filename: string }> => {
  314. const response = await serverFetch(
  315. `${BASE_API_URL}/truck/exportTruckLaneVersionReportExcel`,
  316. {
  317. method: "POST",
  318. headers: {
  319. "Content-Type": "application/json",
  320. Accept:
  321. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  322. },
  323. body: JSON.stringify({ fromVersionId, toVersionId }),
  324. },
  325. );
  326. if (!response.ok) {
  327. const text = await response.text().catch(() => "");
  328. throw new ServerFetchError(
  329. `Export failed: ${response.status} ${text}`.trim(),
  330. response,
  331. );
  332. }
  333. const cd = response.headers.get("content-disposition") ?? "";
  334. let filename = `車線版本報告_${Date.now()}.xlsx`;
  335. const quoted = /filename="([^"]+)"/i.exec(cd);
  336. const star = /filename\*=UTF-8''([^;\s]+)/i.exec(cd);
  337. const raw = (star?.[1] || quoted?.[1])?.trim();
  338. if (raw) {
  339. try {
  340. filename = decodeURIComponent(raw);
  341. } catch {
  342. filename = raw;
  343. }
  344. }
  345. const buf = await response.arrayBuffer();
  346. return {
  347. base64: Buffer.from(buf).toString("base64"),
  348. filename,
  349. };
  350. };
  351. export const importRouteLanesExcelAction = async (
  352. formData: FormData,
  353. ): Promise<MessageResponse> => {
  354. const response = await serverFetch(`${BASE_API_URL}/truck/importRouteLanesExcel`, {
  355. method: "POST",
  356. body: formData,
  357. });
  358. if (!response.ok) {
  359. const text = await response.text().catch(() => "");
  360. throw new ServerFetchError(
  361. `Import failed: ${response.status} ${text}`.trim(),
  362. response,
  363. );
  364. }
  365. return (await response.json()) as MessageResponse;
  366. };
  367. export type RouteLaneImportPreviewRow = {
  368. truckRowId: number | null;
  369. truckLanceCode: string;
  370. remark: string | null;
  371. storeId: string;
  372. departureTime: string;
  373. shopId: number;
  374. shopName: string;
  375. shopCode: string;
  376. loadingSequence: number;
  377. districtReference: string | null;
  378. logisticId: number | null;
  379. };
  380. export type ParseRouteLanesExcelResponse = {
  381. sheetCount: number;
  382. rowCount: number;
  383. rows: RouteLaneImportPreviewRow[];
  384. };
  385. export const parseRouteLanesExcelAction = async (
  386. formData: FormData,
  387. ): Promise<ParseRouteLanesExcelResponse> => {
  388. const response = await serverFetch(`${BASE_API_URL}/truck/parseRouteLanesExcel`, {
  389. method: "POST",
  390. body: formData,
  391. });
  392. if (!response.ok) {
  393. const text = await response.text().catch(() => "");
  394. throw new ServerFetchError(
  395. `Parse import failed: ${response.status} ${text}`.trim(),
  396. response,
  397. );
  398. }
  399. return (await response.json()) as ParseRouteLanesExcelResponse;
  400. };
  401. export const findAllUniqueShopNamesAndCodesFromTrucksAction = cache(async () => {
  402. const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesAndCodesFromTrucks`;
  403. return serverFetchJson<Array<{ name: string; code: string }>>(endpoint, {
  404. method: "GET",
  405. headers: { "Content-Type": "application/json" },
  406. });
  407. });
  408. export const findAllUniqueRemarksFromTrucksAction = cache(async () => {
  409. const endpoint = `${BASE_API_URL}/truck/findAllUniqueRemarksFromTrucks`;
  410. return serverFetchJson<string[]>(endpoint, {
  411. method: "GET",
  412. headers: { "Content-Type": "application/json" },
  413. });
  414. });
  415. export const findAllUniqueShopCodesFromTrucksAction = cache(async () => {
  416. const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopCodesFromTrucks`;
  417. return serverFetchJson<string[]>(endpoint, {
  418. method: "GET",
  419. headers: { "Content-Type": "application/json" },
  420. });
  421. });
  422. export const findAllUniqueShopNamesFromTrucksAction = cache(async () => {
  423. const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesFromTrucks`;
  424. return serverFetchJson<string[]>(endpoint, {
  425. method: "GET",
  426. headers: { "Content-Type": "application/json" },
  427. });
  428. });
  429. // ---- Truck lane version snapshot (DB snapshot) ----
  430. export interface CreateTruckLaneSnapshotRequest {
  431. truckLanceCode?: string | null;
  432. note?: string | null;
  433. }
  434. export interface TruckLaneVersionResponse {
  435. id: number;
  436. truckLanceCode: string;
  437. note: string | null;
  438. created: string | null;
  439. /** truck_lane_version.modifiedBy(BaseEntity) */
  440. modifiedBy?: string | null;
  441. }
  442. export interface TruckLaneVersionLineResponse {
  443. truckRowId: number;
  444. truckLanceCode: string | null;
  445. shopCode: string | null;
  446. branchName: string | null;
  447. districtReference: string | null;
  448. loadingSequence: number | null;
  449. departureTime: string | null;
  450. storeId: string;
  451. remark: string | null;
  452. logisticId: number | null;
  453. }
  454. export type DiffFieldChange = {
  455. field: string;
  456. from: string | null;
  457. to: string | null;
  458. };
  459. export type TruckLaneVersionDiffLine = {
  460. truckRowId: number;
  461. shopCode: string | null;
  462. changes: DiffFieldChange[];
  463. };
  464. export type LogisticMasterDiffLine = {
  465. logisticId: number;
  466. type: string;
  467. logisticName: string;
  468. carPlate: string;
  469. changeText: string;
  470. };
  471. export type TruckLaneVersionDiffResponse = {
  472. fromVersionId: number;
  473. toVersionId: number;
  474. changed: TruckLaneVersionDiffLine[];
  475. logisticMasterChanges?: LogisticMasterDiffLine[];
  476. };
  477. export const createTruckLaneSnapshotAction = async (data: CreateTruckLaneSnapshotRequest) => {
  478. const endpoint = `${BASE_API_URL}/truckLaneVersion/snapshot`;
  479. return serverFetchJson<TruckLaneVersionResponse>(endpoint, {
  480. method: "POST",
  481. body: JSON.stringify(data),
  482. headers: { "Content-Type": "application/json" },
  483. });
  484. };
  485. export const listTruckLaneVersionsAction = cache(async (truckLanceCode?: string | null) => {
  486. const endpoint = `${BASE_API_URL}/truckLaneVersion`;
  487. const url =
  488. truckLanceCode != null && String(truckLanceCode).trim() !== ""
  489. ? `${endpoint}?truckLanceCode=${encodeURIComponent(String(truckLanceCode))}`
  490. : endpoint;
  491. return serverFetchJson<TruckLaneVersionResponse[]>(url, {
  492. method: "GET",
  493. headers: { "Content-Type": "application/json" },
  494. });
  495. });
  496. export const getTruckLaneVersionLinesAction = cache(async (versionId: number) => {
  497. const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/lines`;
  498. return serverFetchJson<TruckLaneVersionLineResponse[]>(endpoint, {
  499. method: "GET",
  500. headers: { "Content-Type": "application/json" },
  501. });
  502. });
  503. export const diffTruckLaneVersionsAction = async (
  504. fromVersionId: number,
  505. toVersionId: number,
  506. ) => {
  507. const endpoint = `${BASE_API_URL}/truckLaneVersion/diff`;
  508. const url = `${endpoint}?fromVersionId=${encodeURIComponent(String(fromVersionId))}&toVersionId=${encodeURIComponent(String(toVersionId))}`;
  509. return serverFetchJson<TruckLaneVersionDiffResponse>(url, {
  510. method: "GET",
  511. headers: { "Content-Type": "application/json" },
  512. });
  513. };
  514. export const restoreTruckLaneVersionAction = async (versionId: number) => {
  515. const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/restore`;
  516. return serverFetchJson<MessageResponse>(endpoint, {
  517. method: "POST",
  518. headers: { "Content-Type": "application/json" },
  519. });
  520. };
  521. export type UpdateTruckLaneVersionNoteRequest = {
  522. note: string | null;
  523. };
  524. export const updateTruckLaneVersionNoteAction = async (
  525. versionId: number,
  526. data: UpdateTruckLaneVersionNoteRequest,
  527. ) => {
  528. const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/note`;
  529. return serverFetchJson<TruckLaneVersionResponse>(endpoint, {
  530. method: "PATCH",
  531. body: JSON.stringify(data),
  532. headers: { "Content-Type": "application/json" },
  533. });
  534. };