FPSMS-frontend
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 

825 rindas
25 KiB

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