FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

584 行
17 KiB

  1. "use client";
  2. import { DoResult } from "@/app/api/do";
  3. import { DoSearchAll, fetchDoSearch, releaseDo ,startBatchReleaseAsync, getBatchReleaseProgress} from "@/app/api/do/actions";
  4. import { useRouter } from "next/navigation";
  5. import React, { ForwardedRef, useCallback, useEffect, useMemo, useState } from "react";
  6. import { useTranslation } from "react-i18next";
  7. import { Criterion } from "../SearchBox";
  8. import { isEmpty, sortBy, uniqBy, upperFirst } from "lodash";
  9. import { arrayToDateString, arrayToDayjs } from "@/app/utils/formatUtil";
  10. import SearchBox from "../SearchBox/SearchBox";
  11. import { EditNote } from "@mui/icons-material";
  12. import InputDataGrid from "../InputDataGrid";
  13. import { CreateConsoDoInput } from "@/app/api/do/actions";
  14. import { TableRow } from "../InputDataGrid/InputDataGrid";
  15. import {
  16. FooterPropsOverrides,
  17. GridColDef,
  18. GridRowModel,
  19. GridToolbarContainer,
  20. useGridApiRef,
  21. } from "@mui/x-data-grid";
  22. import {
  23. FormProvider,
  24. SubmitErrorHandler,
  25. SubmitHandler,
  26. useForm,
  27. } from "react-hook-form";
  28. import { Box, Button, Grid, Stack, Typography, TablePagination} from "@mui/material";
  29. import StyledDataGrid from "../StyledDataGrid";
  30. import { GridRowSelectionModel } from "@mui/x-data-grid";
  31. import Swal from "sweetalert2";
  32. import { useSession } from "next-auth/react";
  33. import { SessionWithTokens } from "@/config/authConfig";
  34. type Props = {
  35. filterArgs?: Record<string, any>;
  36. searchQuery?: Record<string, any>;
  37. onDeliveryOrderSearch: () => void;
  38. };
  39. type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" , string>;
  40. type SearchParamNames = keyof SearchBoxInputs;
  41. // put all this into a new component
  42. // ConsoDoForm
  43. type EntryError =
  44. | {
  45. [field in keyof DoResult]?: string;
  46. }
  47. | undefined;
  48. type DoRow = TableRow<Partial<DoResult>, EntryError>;
  49. const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSearch}) => {
  50. const apiRef = useGridApiRef();
  51. const formProps = useForm<CreateConsoDoInput>({
  52. defaultValues: {},
  53. });
  54. const errors = formProps.formState.errors;
  55. const { t } = useTranslation("do");
  56. const router = useRouter();
  57. const { data: session } = useSession() as { data: SessionWithTokens | null };
  58. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  59. console.log("🔍 DoSearch - session:", session);
  60. console.log("🔍 DoSearch - currentUserId:", currentUserId);
  61. const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
  62. const [rowSelectionModel, setRowSelectionModel] =
  63. useState<GridRowSelectionModel>([]);
  64. const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]);
  65. const [pagingController, setPagingController] = useState({
  66. pageNum: 1,
  67. pageSize: 10,
  68. });
  69. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  70. const newPagingController = {
  71. ...pagingController,
  72. pageNum: newPage + 1,
  73. };
  74. setPagingController(newPagingController);
  75. },[pagingController]);
  76. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  77. const newPageSize = parseInt(event.target.value, 10);
  78. const newPagingController = {
  79. pageNum: 1,
  80. pageSize: newPageSize,
  81. };
  82. setPagingController(newPagingController);
  83. }, []);
  84. const pagedRows = useMemo(() => {
  85. const start = (pagingController.pageNum - 1) * pagingController.pageSize;
  86. return searchAllDos.slice(start, start + pagingController.pageSize);
  87. }, [searchAllDos, pagingController]);
  88. const [currentSearchParams, setCurrentSearchParams] = useState<SearchBoxInputs>({
  89. code: "",
  90. status: "",
  91. estimatedArrivalDate: "",
  92. orderDate: "",
  93. supplierName: "",
  94. shopName: "",
  95. deliveryOrderLines: "",
  96. codeTo: "",
  97. statusTo: "",
  98. estimatedArrivalDateTo: "",
  99. orderDateTo: "",
  100. supplierNameTo: "",
  101. shopNameTo: "",
  102. deliveryOrderLinesTo: ""
  103. });
  104. const [hasSearched, setHasSearched] = useState(false);
  105. const [hasResults, setHasResults] = useState(false);
  106. useEffect(() =>{
  107. setPagingController(p => ({
  108. ...p,
  109. pageNum: 1,
  110. }));
  111. }, [searchAllDos]);
  112. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  113. () => [
  114. { label: t("Code"), paramName: "code", type: "text" },
  115. /*
  116. {
  117. label: t("Order Date From"),
  118. label2: t("Order Date To"),
  119. paramName: "orderDate",
  120. type: "dateRange",
  121. },
  122. */
  123. { label: t("Shop Name"), paramName: "shopName", type: "text" },
  124. {
  125. label: t("Estimated Arrival"),
  126. //label2: t("Estimated Arrival To"),
  127. paramName: "estimatedArrivalDate",
  128. type: "date",
  129. },
  130. {
  131. label: t("Status"),
  132. paramName: "status",
  133. type: "autocomplete",
  134. options:[
  135. {label: t('Pending'), value: 'pending'},
  136. {label: t('Receiving'), value: 'receiving'},
  137. {label: t('Completed'), value: 'completed'}
  138. ]
  139. }
  140. ],
  141. [t],
  142. );
  143. const onReset = useCallback(async () => {
  144. try {
  145. setSearchAllDos([]);
  146. setHasSearched(false);
  147. setHasResults(false);
  148. }
  149. catch (error) {
  150. console.error("Error: ", error);
  151. setSearchAllDos([]);
  152. }
  153. }, []);
  154. const onDetailClick = useCallback(
  155. (doResult: DoResult) => {
  156. if (typeof window !== 'undefined') {
  157. sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams));
  158. }
  159. router.push(`/do/edit?id=${doResult.id}`);
  160. },
  161. [router],
  162. );
  163. const validationTest = useCallback(
  164. (
  165. newRow: GridRowModel<DoRow>,
  166. // rowModel: GridRowSelectionModel
  167. ): EntryError => {
  168. const error: EntryError = {};
  169. console.log(newRow);
  170. // if (!newRow.lowerLimit) {
  171. // error["lowerLimit"] = "lower limit cannot be null"
  172. // }
  173. // if (newRow.lowerLimit && newRow.upperLimit && newRow.lowerLimit > newRow.upperLimit) {
  174. // error["lowerLimit"] = "lower limit should not be greater than upper limit"
  175. // error["upperLimit"] = "lower limit should not be greater than upper limit"
  176. // }
  177. return Object.keys(error).length > 0 ? error : undefined;
  178. },
  179. [],
  180. );
  181. const columns = useMemo<GridColDef[]>(
  182. () => [
  183. // {
  184. // name: "id",
  185. // label: t("Details"),
  186. // onClick: onDetailClick,
  187. // buttonIcon: <EditNote />,
  188. // },
  189. {
  190. field: "id",
  191. headerName: t("Details"),
  192. width: 100,
  193. renderCell: (params) => (
  194. <Button
  195. variant="outlined"
  196. size="small"
  197. startIcon={<EditNote />}
  198. onClick={() => onDetailClick(params.row)}
  199. >
  200. {t("Details")}
  201. </Button>
  202. ),
  203. },
  204. {
  205. field: "code",
  206. headerName: t("code"),
  207. flex: 1.5,
  208. },
  209. {
  210. field: "shopName",
  211. headerName: t("Shop Name"),
  212. flex: 1,
  213. },
  214. {
  215. field: "supplierName",
  216. headerName: t("Supplier Name"),
  217. flex: 1,
  218. },
  219. {
  220. field: "orderDate",
  221. headerName: t("Order Date"),
  222. flex: 1,
  223. renderCell: (params) => {
  224. return params.row.orderDate
  225. ? arrayToDateString(params.row.orderDate)
  226. : "N/A";
  227. },
  228. },
  229. {
  230. field: "estimatedArrivalDate",
  231. headerName: t("Estimated Arrival"),
  232. flex: 1,
  233. renderCell: (params) => {
  234. return params.row.estimatedArrivalDate
  235. ? arrayToDateString(params.row.estimatedArrivalDate)
  236. : "N/A";
  237. },
  238. },
  239. {
  240. field: "status",
  241. headerName: t("Status"),
  242. flex: 1,
  243. renderCell: (params) => {
  244. return t(upperFirst(params.row.status));
  245. },
  246. },
  247. ],
  248. [t, arrayToDateString],
  249. );
  250. const onSubmit = useCallback<SubmitHandler<CreateConsoDoInput>>(
  251. async (data, event) => {
  252. const hasErrors = false;
  253. console.log(errors);
  254. },
  255. [],
  256. );
  257. const onSubmitError = useCallback<SubmitErrorHandler<CreateConsoDoInput>>(
  258. (errors) => {},
  259. [],
  260. );
  261. //SEARCH FUNCTION
  262. const handleSearch = useCallback(async (query: SearchBoxInputs) => {
  263. try {
  264. setCurrentSearchParams(query);
  265. let orderStartDate = "";
  266. let orderEndDate = "";
  267. let estArrStartDate = query.estimatedArrivalDate;
  268. let estArrEndDate = query.estimatedArrivalDate;
  269. const time = "T00:00:00";
  270. //if(orderStartDate != ""){
  271. // orderStartDate = query.orderDate + time;
  272. //}
  273. //if(orderEndDate != ""){
  274. // orderEndDate = query.orderDateTo + time;
  275. //}
  276. if(estArrStartDate != ""){
  277. estArrStartDate = query.estimatedArrivalDate + time;
  278. }
  279. if(estArrEndDate != ""){
  280. estArrEndDate = query.estimatedArrivalDate + time;
  281. }
  282. let status = "";
  283. if(query.status == "All"){
  284. status = "";
  285. }
  286. else{
  287. status = query.status;
  288. }
  289. const data = await fetchDoSearch(
  290. query.code || "",
  291. query.shopName || "",
  292. status,
  293. orderStartDate,
  294. orderEndDate,
  295. estArrStartDate,
  296. estArrEndDate
  297. );
  298. setSearchAllDos(data);
  299. setHasSearched(true);
  300. setHasResults(data.length > 0);
  301. } catch (error) {
  302. console.error("Error: ", error);
  303. setSearchAllDos([]);
  304. setHasSearched(true);
  305. setHasResults(false);
  306. }
  307. }, []);
  308. useEffect(() => {
  309. if (typeof window !== 'undefined') {
  310. const savedSearchParams = sessionStorage.getItem('doSearchParams');
  311. if (savedSearchParams) {
  312. try {
  313. const params = JSON.parse(savedSearchParams);
  314. setCurrentSearchParams(params);
  315. // 自动使用保存的搜索条件重新搜索,获取最新数据
  316. const timer = setTimeout(async () => {
  317. await handleSearch(params);
  318. // 搜索完成后,清除 sessionStorage
  319. if (typeof window !== 'undefined') {
  320. sessionStorage.removeItem('doSearchParams');
  321. sessionStorage.removeItem('doSearchResults');
  322. sessionStorage.removeItem('doSearchHasSearched');
  323. }
  324. }, 100);
  325. return () => clearTimeout(timer);
  326. } catch (e) {
  327. console.error('Error restoring search state:', e);
  328. // 如果出错,也清除 sessionStorage
  329. if (typeof window !== 'undefined') {
  330. sessionStorage.removeItem('doSearchParams');
  331. sessionStorage.removeItem('doSearchResults');
  332. sessionStorage.removeItem('doSearchHasSearched');
  333. }
  334. }
  335. }
  336. }
  337. }, [handleSearch]);
  338. const debouncedSearch = useCallback((query: SearchBoxInputs) => {
  339. if (searchTimeout) {
  340. clearTimeout(searchTimeout);
  341. }
  342. const timeout = setTimeout(() => {
  343. handleSearch(query);
  344. }, 300);
  345. setSearchTimeout(timeout);
  346. }, [handleSearch, searchTimeout]);
  347. const handleBatchRelease = useCallback(async () => {
  348. const totalDeliveryOrderLines = searchAllDos.reduce((sum, doItem) => {
  349. return sum + (doItem.deliveryOrderLines?.length || 0);
  350. }, 0);
  351. const result = await Swal.fire({
  352. icon: "question",
  353. title: t("Batch Release"),
  354. html: `
  355. <div>
  356. <p>${t("Selected Shop(s): ")}${searchAllDos.length}</p>
  357. <p>${t("Selected Item(s): ")}${totalDeliveryOrderLines}</p>
  358. </div>
  359. `,
  360. showCancelButton: true,
  361. confirmButtonText: t("Confirm"),
  362. cancelButtonText: t("Cancel"),
  363. confirmButtonColor: "#8dba00",
  364. cancelButtonColor: "#F04438"
  365. });
  366. if (result.isConfirmed) {
  367. const idsToRelease = searchAllDos.map(d => d.id);
  368. try {
  369. const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
  370. const jobId = startRes?.entity?.jobId;
  371. if (!jobId) {
  372. await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") });
  373. return;
  374. }
  375. const progressSwal = Swal.fire({
  376. title: t("Releasing"),
  377. text: "0% (0 / 0)",
  378. allowOutsideClick: false,
  379. allowEscapeKey: false,
  380. showConfirmButton: false,
  381. didOpen: () => {
  382. Swal.showLoading();
  383. }
  384. });
  385. const timer = setInterval(async () => {
  386. try {
  387. const p = await getBatchReleaseProgress(jobId);
  388. const e = p?.entity || {};
  389. const total = e.total ?? 0;
  390. const finished = e.finished ?? 0;
  391. const percentage = total > 0 ? Math.round((finished / total) * 100) : 0;
  392. const textContent = document.querySelector('.swal2-html-container');
  393. if (textContent) {
  394. textContent.textContent = `${percentage}% (${finished} / ${total})`;
  395. }
  396. if (p.code === "FINISHED" || e.running === false) {
  397. clearInterval(timer);
  398. await new Promise(resolve => setTimeout(resolve, 500));
  399. Swal.close();
  400. await Swal.fire({
  401. icon: "success",
  402. title: t("Completed"),
  403. text: t("Batch release completed successfully."),
  404. confirmButtonText: t("Confirm"),
  405. confirmButtonColor: "#8dba00"
  406. });
  407. if (currentSearchParams && Object.keys(currentSearchParams).length > 0) {
  408. await handleSearch(currentSearchParams);
  409. }
  410. }
  411. } catch (err) {
  412. console.error("progress poll error:", err);
  413. }
  414. }, 800);
  415. } catch (error) {
  416. console.error("Batch release error:", error);
  417. await Swal.fire({
  418. icon: "error",
  419. title: t("Error"),
  420. text: t("An error occurred during batch release"),
  421. confirmButtonText: t("OK")
  422. });
  423. }}
  424. }, [t, currentUserId, searchAllDos, currentSearchParams, handleSearch]);
  425. return (
  426. <>
  427. <FormProvider {...formProps}>
  428. <Stack
  429. spacing={2}
  430. component="form"
  431. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  432. >
  433. <Grid container>
  434. <Grid item xs={8}>
  435. <Typography variant="h4" marginInlineEnd={2}>
  436. {t("Delivery Order")}
  437. </Typography>
  438. </Grid>
  439. <Grid
  440. item
  441. xs={4}
  442. display="flex"
  443. justifyContent="end"
  444. alignItems="end"
  445. >
  446. <Stack spacing={2} direction="row">
  447. {/*<Button
  448. name="submit"
  449. variant="contained"
  450. // startIcon={<Check />}
  451. type="submit"
  452. >
  453. {t("Create")}
  454. </Button>*/}
  455. {hasSearched && hasResults && (
  456. <Button
  457. name="batch_release"
  458. variant="contained"
  459. onClick={handleBatchRelease}
  460. >
  461. {t("Batch Release")}
  462. </Button>
  463. )}
  464. </Stack>
  465. </Grid>
  466. </Grid>
  467. <SearchBox
  468. criteria={searchCriteria}
  469. onSearch={handleSearch}
  470. onReset={onReset}
  471. />
  472. <StyledDataGrid
  473. rows={pagedRows}
  474. columns={columns}
  475. checkboxSelection
  476. rowSelectionModel={rowSelectionModel}
  477. onRowSelectionModelChange={(newRowSelectionModel) => {
  478. setRowSelectionModel(newRowSelectionModel);
  479. formProps.setValue("ids", newRowSelectionModel);
  480. }}
  481. slots={{
  482. footer: FooterToolbar,
  483. noRowsOverlay: NoRowsOverlay,
  484. }}
  485. />
  486. <TablePagination
  487. component="div"
  488. count={searchAllDos.length}
  489. page={(pagingController.pageNum - 1)}
  490. rowsPerPage={pagingController.pageSize}
  491. onPageChange={handlePageChange}
  492. onRowsPerPageChange={handlePageSizeChange}
  493. rowsPerPageOptions={[10, 25, 50]}
  494. />
  495. </Stack>
  496. </FormProvider>
  497. </>
  498. );
  499. };
  500. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  501. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  502. };
  503. const NoRowsOverlay: React.FC = () => {
  504. const { t } = useTranslation("home");
  505. return (
  506. <Box
  507. display="flex"
  508. justifyContent="center"
  509. alignItems="center"
  510. height="100%"
  511. >
  512. <Typography variant="caption">{t("Add some entries!")}</Typography>
  513. </Box>
  514. );
  515. };
  516. export default DoSearch;