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

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