"use client"; import { DoResult } from "@/app/api/do"; import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo ,startBatchReleaseAsync, getBatchReleaseProgress} from "@/app/api/do/actions"; import { useRouter } from "next/navigation"; import React, { ForwardedRef, useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Criterion } from "../SearchBox"; import { isEmpty, sortBy, uniqBy, upperFirst } from "lodash"; import { arrayToDateString, arrayToDayjs } from "@/app/utils/formatUtil"; import SearchBox from "../SearchBox/SearchBox"; import { EditNote } from "@mui/icons-material"; import InputDataGrid from "../InputDataGrid"; import { CreateConsoDoInput } from "@/app/api/do/actions"; import { TableRow } from "../InputDataGrid/InputDataGrid"; import { FooterPropsOverrides, GridColDef, GridRowModel, GridToolbarContainer, useGridApiRef, } from "@mui/x-data-grid"; import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm, } from "react-hook-form"; import { Box, Button, Grid, Stack, Typography, TablePagination} from "@mui/material"; import StyledDataGrid from "../StyledDataGrid"; import { GridRowSelectionModel } from "@mui/x-data-grid"; import Swal from "sweetalert2"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; type Props = { filterArgs?: Record; searchQuery?: Record; onDeliveryOrderSearch: () => void; }; type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" , string>; type SearchParamNames = keyof SearchBoxInputs; // put all this into a new component // ConsoDoForm type EntryError = | { [field in keyof DoResult]?: string; } | undefined; type DoRow = TableRow, EntryError>; const DoSearch: React.FC = ({filterArgs, searchQuery, onDeliveryOrderSearch}) => { const apiRef = useGridApiRef(); const formProps = useForm({ defaultValues: {}, }); const errors = formProps.formState.errors; const { t } = useTranslation("do"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; console.log("🔍 DoSearch - session:", session); console.log("🔍 DoSearch - currentUserId:", currentUserId); const [searchTimeout, setSearchTimeout] = useState(null); const [rowSelectionModel, setRowSelectionModel] = useState([]); const [searchAllDos, setSearchAllDos] = useState([]); const [totalCount, setTotalCount] = useState(0); const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10, }); const [currentSearchParams, setCurrentSearchParams] = useState({ code: "", status: "", estimatedArrivalDate: "", orderDate: "", supplierName: "", shopName: "", deliveryOrderLines: "", codeTo: "", statusTo: "", estimatedArrivalDateTo: "", orderDateTo: "", supplierNameTo: "", shopNameTo: "", deliveryOrderLinesTo: "" }); const [hasSearched, setHasSearched] = useState(false); const [hasResults, setHasResults] = useState(false); // 当搜索条件变化时,重置到第一页 useEffect(() => { setPagingController(p => ({ ...p, pageNum: 1, })); }, [currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate]); const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Shop Name"), paramName: "shopName", type: "text" }, { label: t("Estimated Arrival"), paramName: "estimatedArrivalDate", type: "date", }, { label: t("Status"), paramName: "status", type: "autocomplete", options:[ {label: t('Pending'), value: 'pending'}, {label: t('Receiving'), value: 'receiving'}, {label: t('Completed'), value: 'completed'} ] } ], [t], ); const onReset = useCallback(async () => { try { setSearchAllDos([]); setTotalCount(0); setHasSearched(false); setHasResults(false); setPagingController({ pageNum: 1, pageSize: 10 }); } catch (error) { console.error("Error: ", error); setSearchAllDos([]); setTotalCount(0); } }, []); const onDetailClick = useCallback( (doResult: DoResult) => { if (typeof window !== 'undefined') { sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams)); } router.push(`/do/edit?id=${doResult.id}`); }, [router, currentSearchParams], ); const validationTest = useCallback( ( newRow: GridRowModel, ): EntryError => { const error: EntryError = {}; console.log(newRow); return Object.keys(error).length > 0 ? error : undefined; }, [], ); const columns = useMemo( () => [ { field: "id", headerName: t("Details"), width: 100, renderCell: (params) => ( ), }, { field: "code", headerName: t("code"), flex: 1.5, }, { field: "shopName", headerName: t("Shop Name"), flex: 1, }, { field: "supplierName", headerName: t("Supplier Name"), flex: 1, }, { field: "orderDate", headerName: t("Order Date"), flex: 1, renderCell: (params) => { return params.row.orderDate ? arrayToDateString(params.row.orderDate) : "N/A"; }, }, { field: "estimatedArrivalDate", headerName: t("Estimated Arrival"), flex: 1, renderCell: (params) => { return params.row.estimatedArrivalDate ? arrayToDateString(params.row.estimatedArrivalDate) : "N/A"; }, }, { field: "status", headerName: t("Status"), flex: 1, renderCell: (params) => { return t(upperFirst(params.row.status)); }, }, ], [t, arrayToDateString, onDetailClick], ); const onSubmit = useCallback>( async (data, event) => { const hasErrors = false; console.log(errors); }, [errors], ); const onSubmitError = useCallback>( (errors) => {}, [], ); //SEARCH FUNCTION const handleSearch = useCallback(async (query: SearchBoxInputs) => { try { setCurrentSearchParams(query); let estArrStartDate = query.estimatedArrivalDate; const time = "T00:00:00"; if(estArrStartDate != ""){ estArrStartDate = query.estimatedArrivalDate + time; } let status = ""; if(query.status == "All"){ status = ""; } else{ status = query.status; } // 调用新的 API,传入分页参数 const response = await fetchDoSearch( query.code || "", query.shopName || "", status, "", // orderStartDate - 不再使用 "", // orderEndDate - 不再使用 estArrStartDate, "", // estArrEndDate - 不再使用 pagingController.pageNum, // 传入当前页码 pagingController.pageSize // 传入每页大小 ); setSearchAllDos(response.records); setTotalCount(response.total); // 设置总记录数 setHasSearched(true); setHasResults(response.records.length > 0); } catch (error) { console.error("Error: ", error); setSearchAllDos([]); setTotalCount(0); setHasSearched(true); setHasResults(false); } }, [pagingController]); useEffect(() => { if (typeof window !== 'undefined') { const savedSearchParams = sessionStorage.getItem('doSearchParams'); if (savedSearchParams) { try { const params = JSON.parse(savedSearchParams); setCurrentSearchParams(params); // 自动使用保存的搜索条件重新搜索,获取最新数据 const timer = setTimeout(async () => { await handleSearch(params); // 搜索完成后,清除 sessionStorage if (typeof window !== 'undefined') { sessionStorage.removeItem('doSearchParams'); sessionStorage.removeItem('doSearchResults'); sessionStorage.removeItem('doSearchHasSearched'); } }, 100); return () => clearTimeout(timer); } catch (e) { console.error('Error restoring search state:', e); // 如果出错,也清除 sessionStorage if (typeof window !== 'undefined') { sessionStorage.removeItem('doSearchParams'); sessionStorage.removeItem('doSearchResults'); sessionStorage.removeItem('doSearchHasSearched'); } } } } }, [handleSearch]); const debouncedSearch = useCallback((query: SearchBoxInputs) => { if (searchTimeout) { clearTimeout(searchTimeout); } const timeout = setTimeout(() => { handleSearch(query); }, 300); setSearchTimeout(timeout); }, [handleSearch, searchTimeout]); // 分页变化时重新搜索 const handlePageChange = useCallback((event: unknown, newPage: number) => { const newPagingController = { ...pagingController, pageNum: newPage + 1, }; setPagingController(newPagingController); // 如果已经搜索过,重新搜索 if (hasSearched && currentSearchParams) { // 使用新的分页参数重新搜索 const searchWithNewPage = async () => { try { let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; if(estArrStartDate != ""){ estArrStartDate = currentSearchParams.estimatedArrivalDate + time; } let status = ""; if(currentSearchParams.status == "All"){ status = ""; } else{ status = currentSearchParams.status; } const response = await fetchDoSearch( currentSearchParams.code || "", currentSearchParams.shopName || "", status, "", "", estArrStartDate, "", newPagingController.pageNum, newPagingController.pageSize ); setSearchAllDos(response.records); setTotalCount(response.total); } catch (error) { console.error("Error: ", error); } }; searchWithNewPage(); } }, [pagingController, hasSearched, currentSearchParams]); const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); const newPagingController = { pageNum: 1, // 改变每页大小时重置到第一页 pageSize: newPageSize, }; setPagingController(newPagingController); // 如果已经搜索过,重新搜索 if (hasSearched && currentSearchParams) { const searchWithNewPageSize = async () => { try { let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; if(estArrStartDate != ""){ estArrStartDate = currentSearchParams.estimatedArrivalDate + time; } let status = ""; if(currentSearchParams.status == "All"){ status = ""; } else{ status = currentSearchParams.status; } const response = await fetchDoSearch( currentSearchParams.code || "", currentSearchParams.shopName || "", status, "", "", estArrStartDate, "", 1, // 重置到第一页 newPageSize ); setSearchAllDos(response.records); setTotalCount(response.total); } catch (error) { console.error("Error: ", error); } }; searchWithNewPageSize(); } }, [hasSearched, currentSearchParams]); const handleBatchRelease = useCallback(async () => { try { // 根据当前搜索条件获取所有匹配的记录(不分页) let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; if(estArrStartDate != ""){ estArrStartDate = currentSearchParams.estimatedArrivalDate + time; } let status = ""; if(currentSearchParams.status == "All"){ status = ""; } else{ status = currentSearchParams.status; } // 显示加载提示 const loadingSwal = Swal.fire({ title: t("Loading"), text: t("Fetching all matching records..."), allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); // 获取所有匹配的记录 const allMatchingDos = await fetchAllDoSearch( currentSearchParams.code || "", currentSearchParams.shopName || "", status, estArrStartDate ); Swal.close(); if (allMatchingDos.length === 0) { await Swal.fire({ icon: "warning", title: t("No Records"), text: t("No matching records found for batch release."), confirmButtonText: t("OK") }); return; } // 显示确认对话框 const result = await Swal.fire({ icon: "question", title: t("Batch Release"), html: `

${t("Selected Shop(s): ")}${allMatchingDos.length}

${currentSearchParams.code ? `${t("Code")}: ${currentSearchParams.code} ` : ""} ${currentSearchParams.shopName ? `${t("Shop Name")}: ${currentSearchParams.shopName} ` : ""} ${currentSearchParams.estimatedArrivalDate ? `${t("Estimated Arrival")}: ${currentSearchParams.estimatedArrivalDate} ` : ""} ${status ? `${t("Status")}: ${status} ` : ""}

`, showCancelButton: true, confirmButtonText: t("Confirm"), cancelButtonText: t("Cancel"), confirmButtonColor: "#8dba00", cancelButtonColor: "#F04438" }); if (result.isConfirmed) { const idsToRelease = allMatchingDos.map(d => d.id); try { const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 }); const jobId = startRes?.entity?.jobId; if (!jobId) { await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") }); return; } const progressSwal = Swal.fire({ title: t("Releasing"), text: "0% (0 / 0)", allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); const timer = setInterval(async () => { try { const p = await getBatchReleaseProgress(jobId); const e = p?.entity || {}; const total = e.total ?? 0; const finished = e.finished ?? 0; const percentage = total > 0 ? Math.round((finished / total) * 100) : 0; const textContent = document.querySelector('.swal2-html-container'); if (textContent) { textContent.textContent = `${percentage}% (${finished} / ${total})`; } if (p.code === "FINISHED" || e.running === false) { clearInterval(timer); await new Promise(resolve => setTimeout(resolve, 500)); Swal.close(); await Swal.fire({ icon: "success", title: t("Completed"), text: t("Batch release completed successfully."), confirmButtonText: t("Confirm"), confirmButtonColor: "#8dba00" }); if (currentSearchParams && Object.keys(currentSearchParams).length > 0) { await handleSearch(currentSearchParams); } } } catch (err) { console.error("progress poll error:", err); } }, 800); } catch (error) { console.error("Batch release error:", error); await Swal.fire({ icon: "error", title: t("Error"), text: t("An error occurred during batch release"), confirmButtonText: t("OK") }); } } } catch (error) { console.error("Error fetching all matching records:", error); await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to fetch matching records"), confirmButtonText: t("OK") }); } }, [t, currentUserId, currentSearchParams, handleSearch]); return ( <> {t("Delivery Order")} {hasSearched && hasResults && ( )} { setRowSelectionModel(newRowSelectionModel); formProps.setValue("ids", newRowSelectionModel); }} slots={{ footer: FooterToolbar, noRowsOverlay: NoRowsOverlay, }} /> ); }; const FooterToolbar: React.FC = ({ child }) => { return {child}; }; const NoRowsOverlay: React.FC = () => { const { t } = useTranslation("home"); return ( {t("Add some entries!")} ); }; export default DoSearch;