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.
 
 

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