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

DoSearch.tsx 23 KiB

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