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

574 行
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. //INITIALIZATION
  113. useEffect(() => {
  114. const loadItems = async () => {
  115. try{
  116. //const itemsData = await fetchDoSearch("","","","","","","");
  117. //setSearchAllDos(itemsData);
  118. }
  119. catch (error){
  120. console.error("Loading Error: ", error);
  121. setSearchAllDos([]);
  122. };
  123. };
  124. loadItems();
  125. console.log("success");
  126. },[]);
  127. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  128. () => [
  129. { label: t("Code"), paramName: "code", type: "text" },
  130. {
  131. label: t("Order Date From"),
  132. label2: t("Order Date To"),
  133. paramName: "orderDate",
  134. type: "dateRange",
  135. },
  136. { label: t("Shop Name"), paramName: "shopName", type: "text" },
  137. {
  138. label: t("Estimated Arrival From"),
  139. label2: t("Estimated Arrival To"),
  140. paramName: "estimatedArrivalDate",
  141. type: "dateRange",
  142. },
  143. {
  144. label: t("Status"),
  145. paramName: "status",
  146. type: "autocomplete",
  147. options:[
  148. {label: t('Pending'), value: 'pending'},
  149. {label: t('Receiving'), value: 'receiving'},
  150. {label: t('Completed'), value: 'completed'}
  151. ]
  152. }
  153. ],
  154. [t],
  155. );
  156. const onReset = useCallback(async () => {
  157. try {
  158. //const data = await fetchDoSearch("", "", "", "", "","","");
  159. //setSearchAllDos(data);
  160. setHasSearched(false);
  161. setHasSearched(false);
  162. }
  163. catch (error) {
  164. console.error("Error: ", error);
  165. setSearchAllDos([]);
  166. }
  167. }, []);
  168. const onDetailClick = useCallback(
  169. (doResult: DoResult) => {
  170. router.push(`/do/edit?id=${doResult.id}`);
  171. },
  172. [router],
  173. );
  174. const validationTest = useCallback(
  175. (
  176. newRow: GridRowModel<DoRow>,
  177. // rowModel: GridRowSelectionModel
  178. ): EntryError => {
  179. const error: EntryError = {};
  180. console.log(newRow);
  181. // if (!newRow.lowerLimit) {
  182. // error["lowerLimit"] = "lower limit cannot be null"
  183. // }
  184. // if (newRow.lowerLimit && newRow.upperLimit && newRow.lowerLimit > newRow.upperLimit) {
  185. // error["lowerLimit"] = "lower limit should not be greater than upper limit"
  186. // error["upperLimit"] = "lower limit should not be greater than upper limit"
  187. // }
  188. return Object.keys(error).length > 0 ? error : undefined;
  189. },
  190. [],
  191. );
  192. const columns = useMemo<GridColDef[]>(
  193. () => [
  194. // {
  195. // name: "id",
  196. // label: t("Details"),
  197. // onClick: onDetailClick,
  198. // buttonIcon: <EditNote />,
  199. // },
  200. {
  201. field: "id",
  202. headerName: t("Details"),
  203. width: 100,
  204. renderCell: (params) => (
  205. <Button
  206. variant="outlined"
  207. size="small"
  208. startIcon={<EditNote />}
  209. onClick={() => onDetailClick(params.row)}
  210. >
  211. {t("Details")}
  212. </Button>
  213. ),
  214. },
  215. {
  216. field: "code",
  217. headerName: t("code"),
  218. flex: 1.5,
  219. },
  220. {
  221. field: "shopName",
  222. headerName: t("Shop Name"),
  223. flex: 1,
  224. },
  225. {
  226. field: "supplierName",
  227. headerName: t("Supplier Name"),
  228. flex: 1,
  229. },
  230. {
  231. field: "orderDate",
  232. headerName: t("Order Date"),
  233. flex: 1,
  234. renderCell: (params) => {
  235. return params.row.orderDate
  236. ? arrayToDateString(params.row.orderDate)
  237. : "N/A";
  238. },
  239. },
  240. {
  241. field: "estimatedArrivalDate",
  242. headerName: t("Estimated Arrival"),
  243. flex: 1,
  244. renderCell: (params) => {
  245. return params.row.estimatedArrivalDate
  246. ? arrayToDateString(params.row.estimatedArrivalDate)
  247. : "N/A";
  248. },
  249. },
  250. {
  251. field: "status",
  252. headerName: t("Status"),
  253. flex: 1,
  254. renderCell: (params) => {
  255. return t(upperFirst(params.row.status));
  256. },
  257. },
  258. ],
  259. [t, arrayToDateString],
  260. );
  261. const onSubmit = useCallback<SubmitHandler<CreateConsoDoInput>>(
  262. async (data, event) => {
  263. const hasErrors = false;
  264. console.log(errors);
  265. },
  266. [],
  267. );
  268. const onSubmitError = useCallback<SubmitErrorHandler<CreateConsoDoInput>>(
  269. (errors) => {},
  270. [],
  271. );
  272. //SEARCH FUNCTION
  273. const handleSearch = useCallback(async (query: SearchBoxInputs) => {
  274. try {
  275. setCurrentSearchParams(query);
  276. let orderStartDate = query.orderDate;
  277. let orderEndDate = query.orderDateTo;
  278. let estArrStartDate = query.estimatedArrivalDate;
  279. let estArrEndDate = query.estimatedArrivalDateTo;
  280. const time = "T00:00:00";
  281. if(orderStartDate != ""){
  282. orderStartDate = query.orderDate + time;
  283. }
  284. if(orderEndDate != ""){
  285. orderEndDate = query.orderDateTo + time;
  286. }
  287. if(estArrStartDate != ""){
  288. estArrStartDate = query.estimatedArrivalDate + time;
  289. }
  290. if(estArrEndDate != ""){
  291. estArrEndDate = query.estimatedArrivalDateTo + time;
  292. }
  293. let status = "";
  294. if(query.status == "All"){
  295. status = "";
  296. }
  297. else{
  298. status = query.status;
  299. }
  300. const data = await fetchDoSearch(
  301. query.code || "",
  302. query.shopName || "",
  303. status,
  304. orderStartDate,
  305. orderEndDate,
  306. estArrStartDate,
  307. estArrEndDate
  308. );
  309. setSearchAllDos(data);
  310. setHasSearched(true);
  311. setHasResults(data.length > 0);
  312. } catch (error) {
  313. console.error("Error: ", error);
  314. }
  315. }, []);
  316. const debouncedSearch = useCallback((query: SearchBoxInputs) => {
  317. if (searchTimeout) {
  318. clearTimeout(searchTimeout);
  319. }
  320. const timeout = setTimeout(() => {
  321. handleSearch(query);
  322. }, 300);
  323. setSearchTimeout(timeout);
  324. }, [handleSearch, searchTimeout]);
  325. const handleBatchRelease = useCallback(async () => {
  326. const selectedIds = rowSelectionModel as number[];
  327. if (!selectedIds.length) return;
  328. console.log("🔍 handleBatchRelease - currentUserId:", currentUserId);
  329. console.log("🔍 handleBatchRelease - selectedIds:", selectedIds);
  330. const result = await Swal.fire({
  331. icon: "question",
  332. title: t("Batch Release"),
  333. html: `
  334. <div>
  335. <p>${t("Selected items on current page")}: ${selectedIds.length}</p>
  336. <p>${t("Total search results")}: ${searchAllDos.length}</p>
  337. <hr>
  338. <p><strong>${t("Choose release option")}:</strong></p>
  339. </div>
  340. `,
  341. showCancelButton: true,
  342. confirmButtonText: t("Release All Search Results"),
  343. cancelButtonText: t("Release Selected Only"),
  344. denyButtonText: t("Cancel"),
  345. showDenyButton: true,
  346. confirmButtonColor: "#8dba00",
  347. cancelButtonColor: "#2196f3",
  348. denyButtonColor: "#F04438"
  349. });
  350. if (result.isDenied) return;
  351. let idsToRelease: number[];
  352. if (result.isConfirmed) {
  353. idsToRelease = searchAllDos.map(d => d.id);
  354. } else {
  355. idsToRelease = selectedIds;
  356. }
  357. try {
  358. const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
  359. const jobId = startRes?.entity?.jobId;
  360. if (!jobId) {
  361. await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") });
  362. return;
  363. }
  364. await Swal.fire({
  365. title: t("Releasing"),
  366. html: `
  367. <div style="text-align:left">
  368. <div id="br-total">${t("Total")}: 0</div>
  369. <div id="br-finished">${t("Finished")}: 0</div>
  370. <div style="margin-top:8px;height:8px;background:#eee;border-radius:4px;">
  371. <div id="br-bar" style="height:8px;width:0%;background:#8dba00;border-radius:4px;"></div>
  372. </div>
  373. </div>
  374. `,
  375. allowOutsideClick: false,
  376. allowEscapeKey: false,
  377. showConfirmButton: false,
  378. didOpen: async () => {
  379. const update = (total:number, finished:number, success:number, failed:number) => {
  380. const bar = document.getElementById("br-bar") as HTMLElement;
  381. const pct = total > 0 ? Math.floor((finished / total) * 100) : 0;
  382. (document.getElementById("br-total") as HTMLElement).innerText = `${t("Total")}: ${total}`;
  383. (document.getElementById("br-finished") as HTMLElement).innerText = `${t("Finished")}: ${finished}`;
  384. if (bar) bar.style.width = `${pct}%`;
  385. };
  386. const timer = setInterval(async () => {
  387. try {
  388. const p = await getBatchReleaseProgress(jobId);
  389. const e = p?.entity || {};
  390. update(e.total ?? 0, e.finished ?? 0, e.success ?? 0, e.failedCount ?? 0);
  391. if (p.code === "FINISHED" || e.running === false) {
  392. clearInterval(timer);
  393. Swal.close();
  394. // 简化完成提示 - 只显示完成,不显示成功/失败统计
  395. await Swal.fire({
  396. icon: "success",
  397. title: t("Completed"),
  398. text: t("Batch release completed"),
  399. confirmButtonText: t("OK")
  400. });
  401. if (currentSearchParams && Object.keys(currentSearchParams).length > 0) {
  402. await handleSearch(currentSearchParams);
  403. }
  404. setRowSelectionModel([]);
  405. }
  406. } catch (err) {
  407. console.error("progress poll error:", err);
  408. }
  409. }, 800);
  410. }
  411. });
  412. } catch (error) {
  413. console.error("Batch release error:", error);
  414. await Swal.fire({
  415. icon: "error",
  416. title: t("Error"),
  417. text: t("An error occurred during batch release"),
  418. confirmButtonText: t("OK")
  419. });
  420. }
  421. }, [rowSelectionModel, t, currentUserId, searchAllDos, currentSearchParams, handleSearch]);
  422. return (
  423. <>
  424. <FormProvider {...formProps}>
  425. <Stack
  426. spacing={2}
  427. component="form"
  428. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  429. >
  430. <Grid container>
  431. <Grid item xs={8}>
  432. <Typography variant="h4" marginInlineEnd={2}>
  433. {t("Delivery Order")}
  434. </Typography>
  435. </Grid>
  436. <Grid
  437. item
  438. xs={4}
  439. display="flex"
  440. justifyContent="end"
  441. alignItems="end"
  442. >
  443. <Stack spacing={2} direction="row">
  444. {/*<Button
  445. name="submit"
  446. variant="contained"
  447. // startIcon={<Check />}
  448. type="submit"
  449. >
  450. {t("Create")}
  451. </Button>*/}
  452. {hasSearched && hasResults && (
  453. <Button
  454. name="batch_release"
  455. variant="contained"
  456. onClick={handleBatchRelease}
  457. >
  458. {t("Batch Release")}
  459. </Button>
  460. )}
  461. </Stack>
  462. </Grid>
  463. </Grid>
  464. <SearchBox
  465. criteria={searchCriteria}
  466. onSearch={handleSearch}
  467. onReset={onReset}
  468. />
  469. <StyledDataGrid
  470. rows={pagedRows}
  471. columns={columns}
  472. checkboxSelection
  473. rowSelectionModel={rowSelectionModel}
  474. onRowSelectionModelChange={(newRowSelectionModel) => {
  475. setRowSelectionModel(newRowSelectionModel);
  476. formProps.setValue("ids", newRowSelectionModel);
  477. }}
  478. slots={{
  479. footer: FooterToolbar,
  480. noRowsOverlay: NoRowsOverlay,
  481. }}
  482. />
  483. <TablePagination
  484. component="div"
  485. count={searchAllDos.length}
  486. page={(pagingController.pageNum - 1)}
  487. rowsPerPage={pagingController.pageSize}
  488. onPageChange={handlePageChange}
  489. onRowsPerPageChange={handlePageSizeChange}
  490. rowsPerPageOptions={[10, 25, 50]}
  491. />
  492. </Stack>
  493. </FormProvider>
  494. </>
  495. );
  496. };
  497. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  498. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  499. };
  500. const NoRowsOverlay: React.FC = () => {
  501. const { t } = useTranslation("home");
  502. return (
  503. <Box
  504. display="flex"
  505. justifyContent="center"
  506. alignItems="center"
  507. height="100%"
  508. >
  509. <Typography variant="caption">{t("Add some entries!")}</Typography>
  510. </Box>
  511. );
  512. };
  513. export default DoSearch;