FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

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