FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

927 linhas
27 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. createdBy?: string | null;
  440. /** truck_lane_version.modifiedBy(BaseEntity) */
  441. modifiedBy?: string | null;
  442. }
  443. export interface TruckLaneVersionLineResponse {
  444. truckRowId: number;
  445. truckLanceCode: string | null;
  446. shopCode: string | null;
  447. branchName: string | null;
  448. districtReference: string | null;
  449. loadingSequence: number | null;
  450. departureTime: string | null;
  451. storeId: string;
  452. remark: string | null;
  453. logisticId: number | null;
  454. }
  455. export type DiffFieldChange = {
  456. field: string;
  457. from: string | null;
  458. to: string | null;
  459. };
  460. export type TruckLaneVersionDiffLine = {
  461. truckRowId: number;
  462. shopCode: string | null;
  463. changes: DiffFieldChange[];
  464. /** 快照車線(僅欄位異動時 changes 不含 truckLanceCode/remark) */
  465. truckLanceCode?: string | null;
  466. remark?: string | null;
  467. };
  468. export type LogisticMasterDiffLine = {
  469. logisticId: number;
  470. type: string;
  471. logisticName: string;
  472. carPlate: string;
  473. changeText: string;
  474. };
  475. export type TruckLaneVersionDiffResponse = {
  476. fromVersionId: number;
  477. toVersionId: number;
  478. changed: TruckLaneVersionDiffLine[];
  479. logisticMasterChanges?: LogisticMasterDiffLine[];
  480. };
  481. export const createTruckLaneSnapshotAction = async (data: CreateTruckLaneSnapshotRequest) => {
  482. const endpoint = `${BASE_API_URL}/truckLaneVersion/snapshot`;
  483. return serverFetchJson<TruckLaneVersionResponse>(endpoint, {
  484. method: "POST",
  485. body: JSON.stringify(data),
  486. headers: { "Content-Type": "application/json" },
  487. });
  488. };
  489. export const listTruckLaneVersionsAction = cache(async (truckLanceCode?: string | null) => {
  490. const endpoint = `${BASE_API_URL}/truckLaneVersion`;
  491. const url =
  492. truckLanceCode != null && String(truckLanceCode).trim() !== ""
  493. ? `${endpoint}?truckLanceCode=${encodeURIComponent(String(truckLanceCode))}`
  494. : endpoint;
  495. return serverFetchJson<TruckLaneVersionResponse[]>(url, {
  496. method: "GET",
  497. headers: { "Content-Type": "application/json" },
  498. });
  499. });
  500. export const getTruckLaneVersionLinesAction = cache(async (versionId: number) => {
  501. const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/lines`;
  502. return serverFetchJson<TruckLaneVersionLineResponse[]>(endpoint, {
  503. method: "GET",
  504. headers: { "Content-Type": "application/json" },
  505. });
  506. });
  507. export const diffTruckLaneVersionsAction = async (
  508. fromVersionId: number,
  509. toVersionId: number,
  510. ) => {
  511. const endpoint = `${BASE_API_URL}/truckLaneVersion/diff`;
  512. const url = `${endpoint}?fromVersionId=${encodeURIComponent(String(fromVersionId))}&toVersionId=${encodeURIComponent(String(toVersionId))}`;
  513. return serverFetchJson<TruckLaneVersionDiffResponse>(url, {
  514. method: "GET",
  515. headers: { "Content-Type": "application/json" },
  516. });
  517. };
  518. export const restoreTruckLaneVersionAction = async (versionId: number) => {
  519. const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/restore`;
  520. return serverFetchJson<MessageResponse>(endpoint, {
  521. method: "POST",
  522. headers: { "Content-Type": "application/json" },
  523. });
  524. };
  525. export type UpdateTruckLaneVersionNoteRequest = {
  526. note: string | null;
  527. };
  528. export const updateTruckLaneVersionNoteAction = async (
  529. versionId: number,
  530. data: UpdateTruckLaneVersionNoteRequest,
  531. ) => {
  532. const endpoint = `${BASE_API_URL}/truckLaneVersion/${versionId}/note`;
  533. return serverFetchJson<TruckLaneVersionResponse>(endpoint, {
  534. method: "PATCH",
  535. body: JSON.stringify(data),
  536. headers: { "Content-Type": "application/json" },
  537. });
  538. };
  539. // ---- Truck lane schedule (server-side apply) ----
  540. export type TruckLaneScheduleLineAction =
  541. | "MOVE"
  542. | "CREATE"
  543. | "DELETE"
  544. | "ENSURE_LANE";
  545. export type TruckLaneMoveTargetRequest = {
  546. truckRowId: number;
  547. toTruckLanceCode: string;
  548. toRemark?: string | null;
  549. toStoreId: string;
  550. toLoadingSequence: number;
  551. toDistrictReference?: string | null;
  552. departureTime?: string | null;
  553. };
  554. export type TruckLaneScheduleLineRequest = {
  555. action: TruckLaneScheduleLineAction;
  556. truckRowId?: number | null;
  557. toTruckLanceCode: string;
  558. toRemark?: string | null;
  559. toStoreId: string;
  560. toLoadingSequence?: number | null;
  561. toDistrictReference?: string | null;
  562. shopId?: number | null;
  563. shopCode?: string | null;
  564. shopName?: string | null;
  565. departureTime?: string | null;
  566. logisticId?: number | null;
  567. };
  568. export type CreateTruckLaneScheduleRequest = {
  569. executeAt: string;
  570. note?: string | null;
  571. lines?: TruckLaneScheduleLineRequest[] | null;
  572. moves?: TruckLaneMoveTargetRequest[] | null;
  573. };
  574. export type TruckLaneScheduleLineResponse = {
  575. id: number;
  576. action: TruckLaneScheduleLineAction;
  577. truckRowId: number | null;
  578. shopCode: string | null;
  579. shopName: string | null;
  580. fromTruckLanceCode: string | null;
  581. fromRemark: string | null;
  582. fromStoreId: string | null;
  583. fromLoadingSequence?: number | null;
  584. fromDistrictReference?: string | null;
  585. fromDepartureTime?: string | null;
  586. toTruckLanceCode: string;
  587. toRemark: string | null;
  588. toStoreId: string;
  589. toDistrictReference?: string | null;
  590. toLoadingSequence?: number | null;
  591. departureTime?: string | null;
  592. lineStatus: string;
  593. errorMessage: string | null;
  594. appliedAt: string | null;
  595. };
  596. export type RouteExcelSchedulePlanPreviewRow = {
  597. action: TruckLaneScheduleLineAction;
  598. truckRowId: number | null;
  599. shopCode: string | null;
  600. shopName: string | null;
  601. toTruckLanceCode: string;
  602. toRemark: string | null;
  603. toStoreId: string;
  604. toLoadingSequence: number | null;
  605. };
  606. export type RouteExcelSchedulePlanError = {
  607. shopCode: string;
  608. shopName: string;
  609. message: string;
  610. };
  611. export type RouteExcelSchedulePlanCounts = {
  612. moves: number;
  613. creates: number;
  614. deletes: number;
  615. ensureLanes: number;
  616. };
  617. export type RouteExcelSchedulePlanResponse = {
  618. sheetCount: number;
  619. rowCount: number;
  620. lines: TruckLaneScheduleLineRequest[];
  621. previews: RouteExcelSchedulePlanPreviewRow[];
  622. errors: RouteExcelSchedulePlanError[];
  623. counts: RouteExcelSchedulePlanCounts;
  624. };
  625. export type TruckLaneScheduleResponse = {
  626. id: number;
  627. executeAt: string;
  628. status: string;
  629. source: string;
  630. note: string | null;
  631. appliedAt: string | null;
  632. errorMessage: string | null;
  633. snapshotVersionId: number | null;
  634. preApplySnapshotVersionId?: number | null;
  635. created: string | null;
  636. createdBy?: string | null;
  637. modifiedBy: string | null;
  638. lines?: TruckLaneScheduleLineResponse[] | null;
  639. lineCounts?: {
  640. total: number;
  641. applied: number;
  642. failed: number;
  643. pending: number;
  644. } | null;
  645. };
  646. export type PendingTruckRowIdsResponse = {
  647. /** 所有開放排程(PENDING/APPLYING)涉及的 truck rows(標記、排程驗證用) */
  648. truckRowIds: number[];
  649. /** 已進入鎖定時間窗(或 APPLYING)的 truck rows,看板不可手改 */
  650. lockedTruckRowIds?: number[];
  651. };
  652. export type TruckLaneScheduleExcelPreviewRow = {
  653. rowIndex: number;
  654. shopCode: string;
  655. toTruckLanceCode: string;
  656. toRemark: string | null;
  657. toStoreId: string;
  658. executeAt: string | null;
  659. truckRowId: number | null;
  660. };
  661. export type TruckLaneScheduleExcelRowError = {
  662. rowIndex: number;
  663. message: string;
  664. };
  665. export type ParseTruckLaneScheduleExcelResponse = {
  666. rowCount: number;
  667. validCount: number;
  668. errorCount: number;
  669. rows: TruckLaneScheduleExcelPreviewRow[];
  670. errors: TruckLaneScheduleExcelRowError[];
  671. };
  672. export const createTruckLaneScheduleAction = async (
  673. data: CreateTruckLaneScheduleRequest,
  674. ): Promise<TruckLaneScheduleResponse> => {
  675. const endpoint = `${BASE_API_URL}/truckLaneSchedule`;
  676. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  677. method: "POST",
  678. body: JSON.stringify(data),
  679. headers: { "Content-Type": "application/json" },
  680. });
  681. };
  682. export const planTruckLaneScheduleFromRouteExcelAction = async (
  683. formData: FormData,
  684. ): Promise<RouteExcelSchedulePlanResponse> => {
  685. const response = await serverFetch(
  686. `${BASE_API_URL}/truckLaneSchedule/planFromRouteExcel`,
  687. {
  688. method: "POST",
  689. body: formData,
  690. },
  691. );
  692. if (!response.ok) {
  693. const text = await response.text().catch(() => "");
  694. throw new ServerFetchError(
  695. `Plan import failed: ${response.status} ${text}`.trim(),
  696. response,
  697. );
  698. }
  699. return (await response.json()) as RouteExcelSchedulePlanResponse;
  700. };
  701. export const listTruckLaneSchedulesAction = async (
  702. status?: string[],
  703. limit: number = 200,
  704. ): Promise<TruckLaneScheduleResponse[]> => {
  705. const params = new URLSearchParams();
  706. for (const s of status ?? []) {
  707. params.append("status", s);
  708. }
  709. params.set("limit", String(limit));
  710. const qs = params.toString();
  711. const base = `${BASE_API_URL}/truckLaneSchedule`;
  712. return serverFetchJson<TruckLaneScheduleResponse[]>(
  713. `${base}${qs ? `?${qs}` : ""}`,
  714. {
  715. method: "GET",
  716. headers: { "Content-Type": "application/json" },
  717. },
  718. );
  719. };
  720. export const getTruckLaneScheduleAction = async (
  721. id: number,
  722. ): Promise<TruckLaneScheduleResponse> => {
  723. const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}`;
  724. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  725. method: "GET",
  726. headers: { "Content-Type": "application/json" },
  727. });
  728. };
  729. export const pendingTruckLaneScheduleShopIdsAction =
  730. async (): Promise<PendingTruckRowIdsResponse> => {
  731. const endpoint = `${BASE_API_URL}/truckLaneSchedule/pendingShopIds`;
  732. return serverFetchJson<PendingTruckRowIdsResponse>(endpoint, {
  733. method: "GET",
  734. headers: { "Content-Type": "application/json" },
  735. });
  736. };
  737. export const cancelTruckLaneScheduleAction = async (
  738. id: number,
  739. ): Promise<TruckLaneScheduleResponse> => {
  740. const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/cancel`;
  741. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  742. method: "POST",
  743. headers: { "Content-Type": "application/json" },
  744. });
  745. };
  746. export const applyNowTruckLaneScheduleAction = async (
  747. id: number,
  748. ): Promise<TruckLaneScheduleResponse> => {
  749. const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/applyNow`;
  750. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  751. method: "POST",
  752. headers: { "Content-Type": "application/json" },
  753. });
  754. };
  755. export type RetryFailedTruckLaneScheduleRequest = {
  756. executeAt?: string | null;
  757. };
  758. export const retryFailedTruckLaneScheduleAction = async (
  759. id: number,
  760. body?: RetryFailedTruckLaneScheduleRequest,
  761. ): Promise<TruckLaneScheduleResponse> => {
  762. const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/retry-failed`;
  763. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  764. method: "POST",
  765. body: JSON.stringify(body ?? {}),
  766. headers: { "Content-Type": "application/json" },
  767. });
  768. };
  769. export const reactivateCancelledTruckLaneScheduleAction = async (
  770. id: number,
  771. body?: RetryFailedTruckLaneScheduleRequest,
  772. ): Promise<TruckLaneScheduleResponse> => {
  773. const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/reactivate`;
  774. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  775. method: "POST",
  776. body: JSON.stringify(body ?? {}),
  777. headers: { "Content-Type": "application/json" },
  778. });
  779. };
  780. export const ignoreTruckLaneScheduleAction = async (
  781. id: number,
  782. ): Promise<TruckLaneScheduleResponse> => {
  783. const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/ignore`;
  784. return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
  785. method: "POST",
  786. headers: { "Content-Type": "application/json" },
  787. });
  788. };
  789. export const parseTruckLaneScheduleExcelAction = async (
  790. formData: FormData,
  791. defaultExecuteAt?: string | null,
  792. ): Promise<ParseTruckLaneScheduleExcelResponse> => {
  793. const qs =
  794. defaultExecuteAt != null && defaultExecuteAt !== ""
  795. ? `?defaultExecuteAt=${encodeURIComponent(defaultExecuteAt)}`
  796. : "";
  797. const response = await serverFetch(
  798. `${BASE_API_URL}/truckLaneSchedule/parseExcel${qs}`,
  799. { method: "POST", body: formData },
  800. );
  801. if (!response.ok) {
  802. const text = await response.text().catch(() => "");
  803. throw new ServerFetchError(
  804. `Parse schedule Excel failed: ${response.status} ${text}`.trim(),
  805. response,
  806. );
  807. }
  808. return (await response.json()) as ParseTruckLaneScheduleExcelResponse;
  809. };
  810. export const importTruckLaneScheduleExcelAction = async (
  811. formData: FormData,
  812. defaultExecuteAt?: string | null,
  813. note?: string | null,
  814. ): Promise<TruckLaneScheduleResponse[]> => {
  815. const params = new URLSearchParams();
  816. if (defaultExecuteAt) params.set("defaultExecuteAt", defaultExecuteAt);
  817. if (note) params.set("note", note);
  818. const qs = params.toString() ? `?${params.toString()}` : "";
  819. const response = await serverFetch(
  820. `${BASE_API_URL}/truckLaneSchedule/importExcel${qs}`,
  821. { method: "POST", body: formData },
  822. );
  823. if (!response.ok) {
  824. const text = await response.text().catch(() => "");
  825. throw new ServerFetchError(
  826. `Import schedule Excel failed: ${response.status} ${text}`.trim(),
  827. response,
  828. );
  829. }
  830. return (await response.json()) as TruckLaneScheduleResponse[];
  831. };