FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

797 lines
24 KiB

  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;