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

DoSearch.tsx 15 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ヶ月前
3ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
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ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  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. router.push(`/do/edit?id=${doResult.id}`);
  157. },
  158. [router],
  159. );
  160. const validationTest = useCallback(
  161. (
  162. newRow: GridRowModel<DoRow>,
  163. // rowModel: GridRowSelectionModel
  164. ): EntryError => {
  165. const error: EntryError = {};
  166. console.log(newRow);
  167. // if (!newRow.lowerLimit) {
  168. // error["lowerLimit"] = "lower limit cannot be null"
  169. // }
  170. // if (newRow.lowerLimit && newRow.upperLimit && newRow.lowerLimit > newRow.upperLimit) {
  171. // error["lowerLimit"] = "lower limit should not be greater than upper limit"
  172. // error["upperLimit"] = "lower limit should not be greater than upper limit"
  173. // }
  174. return Object.keys(error).length > 0 ? error : undefined;
  175. },
  176. [],
  177. );
  178. const columns = useMemo<GridColDef[]>(
  179. () => [
  180. // {
  181. // name: "id",
  182. // label: t("Details"),
  183. // onClick: onDetailClick,
  184. // buttonIcon: <EditNote />,
  185. // },
  186. {
  187. field: "id",
  188. headerName: t("Details"),
  189. width: 100,
  190. renderCell: (params) => (
  191. <Button
  192. variant="outlined"
  193. size="small"
  194. startIcon={<EditNote />}
  195. onClick={() => onDetailClick(params.row)}
  196. >
  197. {t("Details")}
  198. </Button>
  199. ),
  200. },
  201. {
  202. field: "code",
  203. headerName: t("code"),
  204. flex: 1.5,
  205. },
  206. {
  207. field: "shopName",
  208. headerName: t("Shop Name"),
  209. flex: 1,
  210. },
  211. {
  212. field: "supplierName",
  213. headerName: t("Supplier Name"),
  214. flex: 1,
  215. },
  216. {
  217. field: "orderDate",
  218. headerName: t("Order Date"),
  219. flex: 1,
  220. renderCell: (params) => {
  221. return params.row.orderDate
  222. ? arrayToDateString(params.row.orderDate)
  223. : "N/A";
  224. },
  225. },
  226. {
  227. field: "estimatedArrivalDate",
  228. headerName: t("Estimated Arrival"),
  229. flex: 1,
  230. renderCell: (params) => {
  231. return params.row.estimatedArrivalDate
  232. ? arrayToDateString(params.row.estimatedArrivalDate)
  233. : "N/A";
  234. },
  235. },
  236. {
  237. field: "status",
  238. headerName: t("Status"),
  239. flex: 1,
  240. renderCell: (params) => {
  241. return t(upperFirst(params.row.status));
  242. },
  243. },
  244. ],
  245. [t, arrayToDateString],
  246. );
  247. const onSubmit = useCallback<SubmitHandler<CreateConsoDoInput>>(
  248. async (data, event) => {
  249. const hasErrors = false;
  250. console.log(errors);
  251. },
  252. [],
  253. );
  254. const onSubmitError = useCallback<SubmitErrorHandler<CreateConsoDoInput>>(
  255. (errors) => {},
  256. [],
  257. );
  258. //SEARCH FUNCTION
  259. const handleSearch = useCallback(async (query: SearchBoxInputs) => {
  260. try {
  261. setCurrentSearchParams(query);
  262. let orderStartDate = "";
  263. let orderEndDate = "";
  264. let estArrStartDate = query.estimatedArrivalDate;
  265. let estArrEndDate = query.estimatedArrivalDate;
  266. const time = "T00:00:00";
  267. //if(orderStartDate != ""){
  268. // orderStartDate = query.orderDate + time;
  269. //}
  270. //if(orderEndDate != ""){
  271. // orderEndDate = query.orderDateTo + time;
  272. //}
  273. if(estArrStartDate != ""){
  274. estArrStartDate = query.estimatedArrivalDate + time;
  275. }
  276. if(estArrEndDate != ""){
  277. estArrEndDate = query.estimatedArrivalDate + time;
  278. }
  279. let status = "";
  280. if(query.status == "All"){
  281. status = "";
  282. }
  283. else{
  284. status = query.status;
  285. }
  286. const data = await fetchDoSearch(
  287. query.code || "",
  288. query.shopName || "",
  289. status,
  290. orderStartDate,
  291. orderEndDate,
  292. estArrStartDate,
  293. estArrEndDate
  294. );
  295. setSearchAllDos(data);
  296. setHasSearched(true);
  297. setHasResults(data.length > 0);
  298. } catch (error) {
  299. console.error("Error: ", error);
  300. setSearchAllDos([]);
  301. setHasSearched(true);
  302. setHasResults(false);
  303. }
  304. }, []);
  305. const debouncedSearch = useCallback((query: SearchBoxInputs) => {
  306. if (searchTimeout) {
  307. clearTimeout(searchTimeout);
  308. }
  309. const timeout = setTimeout(() => {
  310. handleSearch(query);
  311. }, 300);
  312. setSearchTimeout(timeout);
  313. }, [handleSearch, searchTimeout]);
  314. const handleBatchRelease = useCallback(async () => {
  315. const totalDeliveryOrderLines = searchAllDos.reduce((sum, doItem) => {
  316. return sum + (doItem.deliveryOrderLines?.length || 0);
  317. }, 0);
  318. const result = await Swal.fire({
  319. icon: "question",
  320. title: t("Batch Release"),
  321. html: `
  322. <div>
  323. <p>${t("Selected Shop(s): ")}${searchAllDos.length}</p>
  324. <p>${t("Selected Item(s): ")}${totalDeliveryOrderLines}</p>
  325. </div>
  326. `,
  327. showCancelButton: true,
  328. confirmButtonText: t("Confirm"),
  329. cancelButtonText: t("Cancel"),
  330. confirmButtonColor: "#8dba00",
  331. cancelButtonColor: "#F04438"
  332. });
  333. if (result.isConfirmed) {
  334. const idsToRelease = searchAllDos.map(d => d.id);
  335. try {
  336. const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
  337. const jobId = startRes?.entity?.jobId;
  338. if (!jobId) {
  339. await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") });
  340. return;
  341. }
  342. const progressSwal = Swal.fire({
  343. title: t("Releasing"),
  344. text: "0% (0 / 0)",
  345. allowOutsideClick: false,
  346. allowEscapeKey: false,
  347. showConfirmButton: false,
  348. didOpen: () => {
  349. Swal.showLoading();
  350. }
  351. });
  352. const timer = setInterval(async () => {
  353. try {
  354. const p = await getBatchReleaseProgress(jobId);
  355. const e = p?.entity || {};
  356. const total = e.total ?? 0;
  357. const finished = e.finished ?? 0;
  358. const percentage = total > 0 ? Math.round((finished / total) * 100) : 0;
  359. const textContent = document.querySelector('.swal2-html-container');
  360. if (textContent) {
  361. textContent.textContent = `${percentage}% (${finished} / ${total})`;
  362. }
  363. if (p.code === "FINISHED" || e.running === false) {
  364. clearInterval(timer);
  365. await new Promise(resolve => setTimeout(resolve, 500));
  366. Swal.close();
  367. await Swal.fire({
  368. icon: "success",
  369. title: t("Completed"),
  370. text: t("Batch release completed successfully."),
  371. confirmButtonText: t("Confirm"),
  372. confirmButtonColor: "#8dba00"
  373. });
  374. if (currentSearchParams && Object.keys(currentSearchParams).length > 0) {
  375. await handleSearch(currentSearchParams);
  376. }
  377. }
  378. } catch (err) {
  379. console.error("progress poll error:", err);
  380. }
  381. }, 800);
  382. } catch (error) {
  383. console.error("Batch release error:", error);
  384. await Swal.fire({
  385. icon: "error",
  386. title: t("Error"),
  387. text: t("An error occurred during batch release"),
  388. confirmButtonText: t("OK")
  389. });
  390. }}
  391. }, [t, currentUserId, searchAllDos, currentSearchParams, handleSearch]);
  392. return (
  393. <>
  394. <FormProvider {...formProps}>
  395. <Stack
  396. spacing={2}
  397. component="form"
  398. onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
  399. >
  400. <Grid container>
  401. <Grid item xs={8}>
  402. <Typography variant="h4" marginInlineEnd={2}>
  403. {t("Delivery Order")}
  404. </Typography>
  405. </Grid>
  406. <Grid
  407. item
  408. xs={4}
  409. display="flex"
  410. justifyContent="end"
  411. alignItems="end"
  412. >
  413. <Stack spacing={2} direction="row">
  414. {/*<Button
  415. name="submit"
  416. variant="contained"
  417. // startIcon={<Check />}
  418. type="submit"
  419. >
  420. {t("Create")}
  421. </Button>*/}
  422. {hasSearched && hasResults && (
  423. <Button
  424. name="batch_release"
  425. variant="contained"
  426. onClick={handleBatchRelease}
  427. >
  428. {t("Batch Release")}
  429. </Button>
  430. )}
  431. </Stack>
  432. </Grid>
  433. </Grid>
  434. <SearchBox
  435. criteria={searchCriteria}
  436. onSearch={handleSearch}
  437. onReset={onReset}
  438. />
  439. <StyledDataGrid
  440. rows={pagedRows}
  441. columns={columns}
  442. checkboxSelection
  443. rowSelectionModel={rowSelectionModel}
  444. onRowSelectionModelChange={(newRowSelectionModel) => {
  445. setRowSelectionModel(newRowSelectionModel);
  446. formProps.setValue("ids", newRowSelectionModel);
  447. }}
  448. slots={{
  449. footer: FooterToolbar,
  450. noRowsOverlay: NoRowsOverlay,
  451. }}
  452. />
  453. <TablePagination
  454. component="div"
  455. count={searchAllDos.length}
  456. page={(pagingController.pageNum - 1)}
  457. rowsPerPage={pagingController.pageSize}
  458. onPageChange={handlePageChange}
  459. onRowsPerPageChange={handlePageSizeChange}
  460. rowsPerPageOptions={[10, 25, 50]}
  461. />
  462. </Stack>
  463. </FormProvider>
  464. </>
  465. );
  466. };
  467. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  468. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  469. };
  470. const NoRowsOverlay: React.FC = () => {
  471. const { t } = useTranslation("home");
  472. return (
  473. <Box
  474. display="flex"
  475. justifyContent="center"
  476. alignItems="center"
  477. height="100%"
  478. >
  479. <Typography variant="caption">{t("Add some entries!")}</Typography>
  480. </Box>
  481. );
  482. };
  483. export default DoSearch;