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

DoSearch.tsx 17 KiB

6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
1ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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;