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

DoSearch.tsx 17 KiB

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