FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

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