FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

DoSearch.tsx 26 KiB

1ヶ月前
9ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
9ヶ月前
9ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
8ヶ月前
8ヶ月前
10ヶ月前
1年前
5ヶ月前
5ヶ月前
1ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
5ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. "use client";
  2. import { DoResult } from "@/app/api/do";
  3. import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo ,startBatchReleaseAsync, getBatchReleaseProgress} from "@/app/api/do/actions";
  4. import {
  5. startWorkbenchBatchReleaseAsyncV2,
  6. getWorkbenchBatchReleaseProgress,
  7. } from "@/app/api/doworkbench/actions";
  8. import { useRouter } from "next/navigation";
  9. import React, { ForwardedRef, useCallback, useEffect, useMemo, useState } from "react";
  10. import { useTranslation } from "react-i18next";
  11. import { Criterion } from "../SearchBox";
  12. import { isEmpty, sortBy, uniqBy } from "lodash";
  13. import { arrayToDateString, arrayToDayjs } from "@/app/utils/formatUtil";
  14. import SearchBox from "../SearchBox/SearchBox";
  15. import { EditNote } from "@mui/icons-material";
  16. import InputDataGrid from "../InputDataGrid";
  17. import { CreateConsoDoInput } from "@/app/api/do/actions";
  18. import { TableRow } from "../InputDataGrid/InputDataGrid";
  19. import {
  20. FooterPropsOverrides,
  21. GridColDef,
  22. GridRowModel,
  23. GridToolbarContainer,
  24. useGridApiRef,
  25. } from "@mui/x-data-grid";
  26. import {
  27. FormProvider,
  28. SubmitErrorHandler,
  29. SubmitHandler,
  30. useForm,
  31. } from "react-hook-form";
  32. import { Box, Button, Paper, Stack, Tab, Tabs, TablePagination, Typography } from "@mui/material";
  33. import StyledDataGrid from "../StyledDataGrid";
  34. import Swal from "sweetalert2";
  35. import { useSession } from "next-auth/react";
  36. import { SessionWithTokens } from "@/config/authConfig";
  37. import { useDoSearchRowSelection } from "./useDoSearchRowSelection";
  38. import DoReplenishmentTab from "./DoReplenishmentTab";
  39. type Props = {
  40. filterArgs?: Record<string, any>;
  41. searchQuery?: Record<string, any>;
  42. onDeliveryOrderSearch?: () => void;
  43. };
  44. type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>;
  45. type SearchParamNames = keyof SearchBoxInputs;
  46. type DoSearchTab = "2F" | "4F" | "TRUCK_X" | "ETRA" | "REPLENISH";
  47. type TabFilter = { floor: "2F" | "4F" | null; isExtra: boolean; forceTruckKeyword?: string };
  48. // put all this into a new component
  49. // ConsoDoForm
  50. type EntryError =
  51. | {
  52. [field in keyof DoResult]?: string;
  53. }
  54. | undefined;
  55. type DoRow = TableRow<Partial<DoResult>, EntryError>;
  56. /** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */
  57. function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean {
  58. return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === "";
  59. }
  60. const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => {
  61. const apiRef = useGridApiRef();
  62. const formProps = useForm<CreateConsoDoInput>({
  63. defaultValues: {},
  64. });
  65. const { setValue } = formProps;
  66. const errors = formProps.formState.errors;
  67. const { t } = useTranslation("do");
  68. const router = useRouter();
  69. const { data: session } = useSession() as { data: SessionWithTokens | null };
  70. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  71. //console.log("🔍 DoSearch - session:", session);
  72. //console.log("🔍 DoSearch - currentUserId:", currentUserId);
  73. const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
  74. const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]);
  75. const [totalCount, setTotalCount] = useState(0);
  76. const [isWorkbench, setIsWorkbench] = useState(false);
  77. const [activeTab, setActiveTab] = useState<DoSearchTab>("2F");
  78. const [searchBoxResetKey, setSearchBoxResetKey] = useState(0);
  79. const [pagingController, setPagingController] = useState({
  80. pageNum: 1,
  81. pageSize: 10,
  82. });
  83. const [currentSearchParams, setCurrentSearchParams] = useState<SearchBoxInputs>({
  84. code: "",
  85. status: "",
  86. estimatedArrivalDate: "",
  87. orderDate: "",
  88. supplierName: "",
  89. shopName: "",
  90. deliveryOrderLines: "",
  91. truckLanceCode: "",
  92. codeTo: "",
  93. statusTo: "",
  94. estimatedArrivalDateTo: "",
  95. orderDateTo: "",
  96. supplierNameTo: "",
  97. shopNameTo: "",
  98. deliveryOrderLinesTo: "",
  99. truckLanceCodeTo: "",
  100. });
  101. const createClearedSearchParams = useCallback(
  102. (source: SearchBoxInputs): SearchBoxInputs => ({
  103. code: "",
  104. status: "",
  105. estimatedArrivalDate: source.estimatedArrivalDate || "",
  106. orderDate: "",
  107. supplierName: "",
  108. shopName: "",
  109. deliveryOrderLines: "",
  110. truckLanceCode: "",
  111. codeTo: "",
  112. statusTo: "",
  113. estimatedArrivalDateTo: source.estimatedArrivalDateTo || "",
  114. orderDateTo: "",
  115. supplierNameTo: "",
  116. shopNameTo: "",
  117. deliveryOrderLinesTo: "",
  118. truckLanceCodeTo: "",
  119. }),
  120. [],
  121. );
  122. const [hasSearched, setHasSearched] = useState(false);
  123. const [hasResults, setHasResults] = useState(false);
  124. const {
  125. rowSelectionModel,
  126. applyRowSelectionChange,
  127. resetSelection,
  128. resolveIdsForBatchRelease,
  129. } = useDoSearchRowSelection(searchAllDos, setValue);
  130. // 当搜索条件变化时,重置到第一页
  131. useEffect(() => {
  132. setPagingController(p => ({
  133. ...p,
  134. pageNum: 1,
  135. }));
  136. }, [
  137. currentSearchParams.code,
  138. currentSearchParams.shopName,
  139. currentSearchParams.status,
  140. currentSearchParams.estimatedArrivalDate,
  141. currentSearchParams.truckLanceCode,
  142. ]);
  143. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  144. () => [
  145. { label: t("Code"), paramName: "code", type: "text" },
  146. { label: t("Shop Name"), paramName: "shopName", type: "text" },
  147. { label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" },
  148. {
  149. label: t("Estimated Arrival"),
  150. paramName: "estimatedArrivalDate",
  151. type: "date",
  152. preFilledValue: currentSearchParams.estimatedArrivalDate || "",
  153. },
  154. {
  155. label: t("Status"),
  156. paramName: "status",
  157. type: "autocomplete",
  158. options:[
  159. {label: t('Pending'), value: 'pending'},
  160. {label: t('Receiving'), value: 'receiving'},
  161. {label: t('Completed'), value: 'completed'}
  162. ]
  163. }
  164. ],
  165. [t, currentSearchParams.estimatedArrivalDate],
  166. );
  167. const onReset = useCallback(async () => {
  168. try {
  169. setSearchAllDos([]);
  170. setTotalCount(0);
  171. setHasSearched(false);
  172. setHasResults(false);
  173. resetSelection();
  174. setPagingController({ pageNum: 1, pageSize: 10 });
  175. }
  176. catch (error) {
  177. console.error("Error: ", error);
  178. setSearchAllDos([]);
  179. setTotalCount(0);
  180. }
  181. }, [resetSelection]);
  182. const onDetailClick = useCallback(
  183. (doResult: DoResult) => {
  184. if (typeof window !== 'undefined') {
  185. sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams));
  186. }
  187. router.push(`/do/edit?id=${doResult.id}`);
  188. },
  189. [router, currentSearchParams],
  190. );
  191. const validationTest = useCallback(
  192. (
  193. newRow: GridRowModel<DoRow>,
  194. ): EntryError => {
  195. const error: EntryError = {};
  196. console.log(newRow);
  197. return Object.keys(error).length > 0 ? error : undefined;
  198. },
  199. [],
  200. );
  201. const columns = useMemo<GridColDef[]>(
  202. () => [
  203. {
  204. field: "id",
  205. headerName: t("Details"),
  206. width: 100,
  207. renderCell: (params) => (
  208. <Button
  209. variant="outlined"
  210. size="small"
  211. startIcon={<EditNote />}
  212. onClick={() => onDetailClick(params.row)}
  213. >
  214. {t("Details")}
  215. </Button>
  216. ),
  217. },
  218. {
  219. field: "code",
  220. headerName: t("code"),
  221. flex: 1.5,
  222. },
  223. {
  224. field: "shopName",
  225. headerName: t("Shop Name"),
  226. flex: 1,
  227. },
  228. {
  229. field: "supplierName",
  230. headerName: t("Supplier Name"),
  231. flex: 1,
  232. },
  233. {
  234. field: "truckLanceCode",
  235. headerName: t("Truck Lance Code"),
  236. flex: 1,
  237. renderCell: (params) => {
  238. const v = params.row.truckLanceCode;
  239. if (v == null) return "車線-X";
  240. if (typeof v === "string" && v.trim() === "") return "車線-X";
  241. return v;
  242. }
  243. },
  244. {
  245. field: "orderDate",
  246. headerName: t("Order Date"),
  247. flex: 1,
  248. renderCell: (params) => {
  249. return params.row.orderDate
  250. ? arrayToDateString(params.row.orderDate)
  251. : "N/A";
  252. },
  253. },
  254. {
  255. field: "estimatedArrivalDate",
  256. headerName: t("Estimated Arrival"),
  257. flex: 1,
  258. renderCell: (params) => {
  259. return params.row.estimatedArrivalDate
  260. ? arrayToDateString(params.row.estimatedArrivalDate)
  261. : "N/A";
  262. },
  263. },
  264. {
  265. field: "status",
  266. headerName: t("Status"),
  267. flex: 1,
  268. renderCell: (params) => {
  269. return t(params.row.status);
  270. },
  271. },
  272. ],
  273. [t, arrayToDateString, onDetailClick],
  274. );
  275. const onSubmit = useCallback<SubmitHandler<CreateConsoDoInput>>(
  276. async (data, event) => {
  277. const hasErrors = false;
  278. console.log(errors);
  279. },
  280. [errors],
  281. );
  282. const onSubmitError = useCallback<SubmitErrorHandler<CreateConsoDoInput>>(
  283. (errors) => {},
  284. [],
  285. );
  286. const resolveTabFilter = useCallback((tab: DoSearchTab): TabFilter => {
  287. switch (tab) {
  288. case "2F":
  289. return { floor: "2F", isExtra: false };
  290. case "4F":
  291. return { floor: "4F", isExtra: false };
  292. case "TRUCK_X":
  293. return { floor: null, isExtra: false, forceTruckKeyword: "x" };
  294. case "ETRA":
  295. return { floor: null, isExtra: true };
  296. case "REPLENISH":
  297. default:
  298. return { floor: null, isExtra: false };
  299. }
  300. }, []);
  301. const performSearch = useCallback(
  302. async (
  303. query: SearchBoxInputs,
  304. pageNum: number,
  305. pageSize: number,
  306. options?: { resetExcludedRows?: boolean; markSearched?: boolean; tabOverride?: DoSearchTab },
  307. ) => {
  308. const effectiveTab = options?.tabOverride ?? activeTab;
  309. const tabFilter = resolveTabFilter(effectiveTab);
  310. const tabTruckKeyword = tabFilter.forceTruckKeyword ?? "";
  311. const effectiveTruckLanceCode = tabTruckKeyword || query.truckLanceCode || "";
  312. const shouldValidateTruckLane = effectiveTab !== "TRUCK_X";
  313. if (
  314. shouldValidateTruckLane &&
  315. isTruckLaneSearchMissingEta(effectiveTruckLanceCode, query.estimatedArrivalDate ?? "")
  316. ) {
  317. await Swal.fire({
  318. icon: "warning",
  319. title: t("Truck lane search requires date title"),
  320. text: t("Truck lane search requires date message"),
  321. confirmButtonText: t("Confirm"),
  322. });
  323. return false;
  324. }
  325. let estArrStartDate = query.estimatedArrivalDate;
  326. const time = "T00:00:00";
  327. if (estArrStartDate !== "") {
  328. estArrStartDate = `${query.estimatedArrivalDate}${time}`;
  329. }
  330. const status = query.status === "All" ? "" : query.status;
  331. const response = await fetchDoSearch(
  332. query.code || "",
  333. query.shopName || "",
  334. status,
  335. "",
  336. "",
  337. estArrStartDate,
  338. "",
  339. pageNum,
  340. pageSize,
  341. effectiveTruckLanceCode,
  342. tabFilter.floor,
  343. tabFilter.isExtra,
  344. );
  345. setSearchAllDos(response.records);
  346. setTotalCount(response.total);
  347. if (options?.markSearched ?? false) {
  348. setHasSearched(true);
  349. setHasResults(response.records.length > 0);
  350. }
  351. if (options?.resetExcludedRows ?? false) {
  352. resetSelection();
  353. }
  354. return true;
  355. },
  356. [activeTab, resolveTabFilter, t, resetSelection],
  357. );
  358. //SEARCH FUNCTION
  359. const handleSearch = useCallback(
  360. async (query: SearchBoxInputs) => {
  361. try {
  362. setCurrentSearchParams(query);
  363. await performSearch(query, pagingController.pageNum, pagingController.pageSize, {
  364. resetExcludedRows: true,
  365. markSearched: true,
  366. });
  367. } catch (error) {
  368. console.error("Error: ", error);
  369. setSearchAllDos([]);
  370. setTotalCount(0);
  371. setHasSearched(true);
  372. setHasResults(false);
  373. resetSelection();
  374. }
  375. },
  376. [pagingController.pageNum, pagingController.pageSize, performSearch, resetSelection],
  377. );
  378. useEffect(() => {
  379. if (typeof window !== 'undefined') {
  380. const savedSearchParams = sessionStorage.getItem('doSearchParams');
  381. if (savedSearchParams) {
  382. try {
  383. const params = JSON.parse(savedSearchParams);
  384. setCurrentSearchParams(params);
  385. // 自动使用保存的搜索条件重新搜索,获取最新数据
  386. const timer = setTimeout(async () => {
  387. await handleSearch(params);
  388. // 搜索完成后,清除 sessionStorage
  389. if (typeof window !== 'undefined') {
  390. sessionStorage.removeItem('doSearchParams');
  391. sessionStorage.removeItem('doSearchResults');
  392. sessionStorage.removeItem('doSearchHasSearched');
  393. }
  394. }, 100);
  395. return () => clearTimeout(timer);
  396. } catch (e) {
  397. console.error('Error restoring search state:', e);
  398. // 如果出错,也清除 sessionStorage
  399. if (typeof window !== 'undefined') {
  400. sessionStorage.removeItem('doSearchParams');
  401. sessionStorage.removeItem('doSearchResults');
  402. sessionStorage.removeItem('doSearchHasSearched');
  403. }
  404. }
  405. }
  406. }
  407. }, [handleSearch]);
  408. const debouncedSearch = useCallback((query: SearchBoxInputs) => {
  409. if (searchTimeout) {
  410. clearTimeout(searchTimeout);
  411. }
  412. const timeout = setTimeout(() => {
  413. handleSearch(query);
  414. }, 300);
  415. setSearchTimeout(timeout);
  416. }, [handleSearch, searchTimeout]);
  417. // 分页变化时重新搜索
  418. const handlePageChange = useCallback(
  419. (event: unknown, newPage: number) => {
  420. const newPagingController = {
  421. ...pagingController,
  422. pageNum: newPage + 1,
  423. };
  424. setPagingController(newPagingController);
  425. if (hasSearched && currentSearchParams) {
  426. void performSearch(
  427. currentSearchParams,
  428. newPagingController.pageNum,
  429. newPagingController.pageSize,
  430. ).catch((error) => {
  431. console.error("Error: ", error);
  432. });
  433. }
  434. },
  435. [pagingController, hasSearched, currentSearchParams, performSearch],
  436. );
  437. const handlePageSizeChange = useCallback(
  438. (event: React.ChangeEvent<HTMLInputElement>) => {
  439. const newPageSize = parseInt(event.target.value, 10);
  440. const newPagingController = {
  441. pageNum: 1,
  442. pageSize: newPageSize,
  443. };
  444. setPagingController(newPagingController);
  445. if (hasSearched && currentSearchParams) {
  446. void performSearch(currentSearchParams, 1, newPageSize).catch((error) => {
  447. console.error("Error: ", error);
  448. });
  449. }
  450. },
  451. [hasSearched, currentSearchParams, performSearch],
  452. );
  453. const handleBatchRelease = useCallback(async (isWorkbench: boolean) => {
  454. try {
  455. const tabFilter = resolveTabFilter(activeTab);
  456. const tabTruckKeyword = tabFilter.forceTruckKeyword ?? "";
  457. const effectiveTruckLanceCode = tabTruckKeyword || currentSearchParams.truckLanceCode || "";
  458. const shouldValidateTruckLane = activeTab !== "TRUCK_X";
  459. if (
  460. shouldValidateTruckLane &&
  461. isTruckLaneSearchMissingEta(
  462. effectiveTruckLanceCode,
  463. currentSearchParams.estimatedArrivalDate ?? "",
  464. )
  465. ) {
  466. await Swal.fire({
  467. icon: "warning",
  468. title: t("Truck lane search requires date title"),
  469. text: t("Truck lane search requires date message"),
  470. confirmButtonText: t("Confirm"),
  471. });
  472. return;
  473. }
  474. // 根据当前搜索条件获取所有匹配的记录(不分页)
  475. let estArrStartDate = currentSearchParams.estimatedArrivalDate;
  476. const time = "T00:00:00";
  477. if(estArrStartDate != ""){
  478. estArrStartDate = currentSearchParams.estimatedArrivalDate + time;
  479. }
  480. let status = "";
  481. if(currentSearchParams.status == "All"){
  482. status = "";
  483. }
  484. else{
  485. status = currentSearchParams.status;
  486. }
  487. // 显示加载提示
  488. const loadingSwal = Swal.fire({
  489. title: t("Loading"),
  490. text: t("Fetching all matching records..."),
  491. allowOutsideClick: false,
  492. allowEscapeKey: false,
  493. showConfirmButton: false,
  494. didOpen: () => {
  495. Swal.showLoading();
  496. }
  497. });
  498. // 获取所有匹配的记录
  499. const allMatchingDos = await fetchAllDoSearch(
  500. currentSearchParams.code || "",
  501. currentSearchParams.shopName || "",
  502. status,
  503. estArrStartDate,
  504. effectiveTruckLanceCode,
  505. tabFilter.floor,
  506. tabFilter.isExtra,
  507. );
  508. Swal.close();
  509. if (allMatchingDos.length === 0) {
  510. await Swal.fire({
  511. icon: "warning",
  512. title: t("No Records"),
  513. text: t("No matching records found for batch release."),
  514. confirmButtonText: t("OK")
  515. });
  516. return;
  517. }
  518. const idsToRelease = resolveIdsForBatchRelease(
  519. allMatchingDos.map((d) => d.id),
  520. );
  521. if (idsToRelease.length === 0) {
  522. await Swal.fire({
  523. icon: "warning",
  524. title: t("No Records"),
  525. text: t("No delivery orders selected for batch release. Uncheck orders you want to exclude, or search again to reset selection."),
  526. confirmButtonText: t("OK"),
  527. });
  528. return;
  529. }
  530. const showMergeExtraOption = isWorkbench && activeTab === "ETRA";
  531. const result = await Swal.fire({
  532. icon: "question",
  533. title: t("Batch Release"),
  534. html: `
  535. <div style="text-align: left;">
  536. <p>${t("Selected Shop(s): ")}${idsToRelease.length}</p>
  537. <p style="font-size: 0.9em; color: #666; margin-top: 8px;">
  538. ${currentSearchParams.code ? `${t("Code")}: ${currentSearchParams.code} ` : ""}
  539. ${currentSearchParams.shopName ? `${t("Shop Name")}: ${currentSearchParams.shopName} ` : ""}
  540. ${currentSearchParams.estimatedArrivalDate ? `${t("Estimated Arrival")}: ${currentSearchParams.estimatedArrivalDate} ` : ""}
  541. ${status ? `${t("Status")}: ${t(status)} ` : ""}
  542. </p>
  543. ${
  544. showMergeExtraOption
  545. ? `<p style="font-size:0.95em;color:#666;margin-top:16px;">${t("Merge extra orders into lane batch ticket")}</p>`
  546. : ""
  547. }
  548. </div>
  549. `,
  550. showCancelButton: true,
  551. showDenyButton: showMergeExtraOption,
  552. confirmButtonText: showMergeExtraOption ? t("Confirm merge release") : t("Confirm"),
  553. denyButtonText: t("Confirm release without merge"),
  554. cancelButtonText: t("Cancel"),
  555. confirmButtonColor: "#8dba00",
  556. denyButtonColor: "#6366f1",
  557. cancelButtonColor: "#F04438",
  558. });
  559. if (result.isDismissed) return;
  560. if (!result.isConfirmed && !result.isDenied) return;
  561. const mergeExtraIntoLaneTicket = showMergeExtraOption ? result.isConfirmed : true;
  562. try {
  563. let startRes ;
  564. if(isWorkbench){
  565. startRes = await startWorkbenchBatchReleaseAsyncV2({
  566. ids: idsToRelease,
  567. userId: currentUserId ?? 1,
  568. mergeExtraIntoLaneTicket,
  569. });
  570. }
  571. else{
  572. startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
  573. }
  574. //await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
  575. const jobId = startRes?.entity?.jobId;
  576. if (!jobId) {
  577. await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") });
  578. return;
  579. }
  580. const progressSwal = Swal.fire({
  581. title: t("Releasing"),
  582. text: "0% (0 / 0)",
  583. allowOutsideClick: false,
  584. allowEscapeKey: false,
  585. showConfirmButton: false,
  586. didOpen: () => {
  587. Swal.showLoading();
  588. }
  589. });
  590. const timer = setInterval(async () => {
  591. try {
  592. const p = isWorkbench
  593. ? await getWorkbenchBatchReleaseProgress(jobId)
  594. : await getBatchReleaseProgress(jobId);
  595. const e = p?.entity || {};
  596. const total = e.total ?? 0;
  597. const finished = e.finished ?? 0;
  598. const percentage = total > 0 ? Math.round((finished / total) * 100) : 0;
  599. const textContent = document.querySelector('.swal2-html-container');
  600. if (textContent) {
  601. textContent.textContent = `${percentage}% (${finished} / ${total})`;
  602. }
  603. if (p.code === "FINISHED" || e.running === false) {
  604. clearInterval(timer);
  605. await new Promise(resolve => setTimeout(resolve, 500));
  606. Swal.close();
  607. await Swal.fire({
  608. icon: "success",
  609. title: t("Completed"),
  610. text: t("Batch release completed successfully."),
  611. confirmButtonText: t("Confirm"),
  612. confirmButtonColor: "#8dba00"
  613. });
  614. if (currentSearchParams && Object.keys(currentSearchParams).length > 0) {
  615. await handleSearch(currentSearchParams);
  616. }
  617. }
  618. } catch (err) {
  619. console.error("progress poll error:", err);
  620. }
  621. }, 800);
  622. } catch (error) {
  623. console.error("Batch release error:", error);
  624. await Swal.fire({
  625. icon: "error",
  626. title: t("Error"),
  627. text: t("An error occurred during batch release"),
  628. confirmButtonText: t("OK")
  629. });
  630. }
  631. } catch (error) {
  632. console.error("Error fetching all matching records:", error);
  633. await Swal.fire({
  634. icon: "error",
  635. title: t("Error"),
  636. text: t("Failed to fetch matching records"),
  637. confirmButtonText: t("OK")
  638. });
  639. }
  640. }, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease, activeTab, resolveTabFilter]);
  641. const handleTabChange = useCallback(
  642. (_: React.SyntheticEvent, nextTab: DoSearchTab) => {
  643. if (nextTab === activeTab) return;
  644. const nextSearchParams = createClearedSearchParams(currentSearchParams);
  645. setActiveTab(nextTab);
  646. setCurrentSearchParams(nextSearchParams);
  647. setSearchBoxResetKey((prev) => prev + 1);
  648. setPagingController((prev) => ({ ...prev, pageNum: 1 }));
  649. resetSelection();
  650. // 切換 tab 僅重置搜索條件與結果;由使用者再次按「搜索」後才查詢。
  651. setSearchAllDos([]);
  652. setTotalCount(0);
  653. setHasSearched(false);
  654. setHasResults(false);
  655. },
  656. [
  657. activeTab,
  658. currentSearchParams,
  659. createClearedSearchParams,
  660. resetSelection,
  661. ],
  662. );
  663. return (
  664. <>
  665. <FormProvider {...formProps}>
  666. <Stack
  667. spacing={2}
  668. component="form"
  669. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  670. >
  671. <Paper variant="outlined" sx={{ px: 2, pt: 1 }}>
  672. <Box display="flex" justifyContent="space-between" alignItems="center">
  673. <Tabs
  674. value={activeTab}
  675. onChange={handleTabChange}
  676. variant="scrollable"
  677. >
  678. <Tab value="2F" label="2/F" />
  679. <Tab value="4F" label="4/F" />
  680. <Tab value="TRUCK_X" label={t("Truck X")} />
  681. <Tab value="ETRA" label={t("Etra")} />
  682. <Tab value="REPLENISH" label={t("Replenishment")} />
  683. </Tabs>
  684. {activeTab !== "REPLENISH" && hasSearched && hasResults && (
  685. <Button
  686. name="batch_release"
  687. variant="contained"
  688. onClick={() => handleBatchRelease(true)}
  689. >
  690. {t("Workbench Batch Release")}
  691. </Button>
  692. )}
  693. </Box>
  694. </Paper>
  695. {activeTab === "REPLENISH" ? (
  696. <DoReplenishmentTab />
  697. ) : (
  698. <>
  699. <SearchBox
  700. key={`tab-reset-${searchBoxResetKey}`}
  701. criteria={searchCriteria}
  702. onSearch={handleSearch}
  703. onReset={onReset}
  704. />
  705. <Paper variant="outlined" sx={{ overflow: "hidden" }}>
  706. <StyledDataGrid
  707. rows={searchAllDos}
  708. columns={columns}
  709. checkboxSelection
  710. rowSelectionModel={rowSelectionModel}
  711. onRowSelectionModelChange={applyRowSelectionChange}
  712. slots={{
  713. footer: FooterToolbar,
  714. noRowsOverlay: NoRowsOverlay,
  715. }}
  716. />
  717. <TablePagination
  718. component="div"
  719. count={totalCount}
  720. page={(pagingController.pageNum - 1)}
  721. rowsPerPage={pagingController.pageSize}
  722. onPageChange={handlePageChange}
  723. onRowsPerPageChange={handlePageSizeChange}
  724. rowsPerPageOptions={[10, 25, 50]}
  725. />
  726. </Paper>
  727. </>
  728. )}
  729. </Stack>
  730. </FormProvider>
  731. </>
  732. );
  733. };
  734. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  735. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  736. };
  737. const NoRowsOverlay: React.FC = () => {
  738. const { t } = useTranslation("home");
  739. return (
  740. <Box
  741. display="flex"
  742. justifyContent="center"
  743. alignItems="center"
  744. height="100%"
  745. >
  746. <Typography variant="caption">{t("Add some entries!")}</Typography>
  747. </Box>
  748. );
  749. };
  750. export default DoSearch;