FPSMS-frontend
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

546 satır
15 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. 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;