FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

DoSearch.tsx 25 KiB

9ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
9ヶ月前
9ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
8ヶ月前
8ヶ月前
4ヶ月前
10ヶ月前
1年前
5ヶ月前
5ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
5ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  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;