"use client"; import { DoResult } from "@/app/api/do"; import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo ,startBatchReleaseAsync, getBatchReleaseProgress} from "@/app/api/do/actions"; import { startWorkbenchBatchReleaseAsyncV2, getWorkbenchBatchReleaseProgress, } from "@/app/api/doworkbench/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 } 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, Paper, Stack, Tab, Tabs, TablePagination, Typography } from "@mui/material"; import StyledDataGrid from "../StyledDataGrid"; import Swal from "sweetalert2"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { useDoSearchRowSelection } from "./useDoSearchRowSelection"; type Props = { filterArgs?: Record; searchQuery?: Record; onDeliveryOrderSearch?: () => void; }; type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>; type SearchParamNames = keyof SearchBoxInputs; type DoSearchTab = "2F" | "4F" | "TRUCK_X" | "ETRA"; type TabFilter = { floor: "2F" | "4F" | null; isExtra: boolean; forceTruckKeyword?: string }; // put all this into a new component // ConsoDoForm type EntryError = | { [field in keyof DoResult]?: string; } | undefined; type DoRow = TableRow, EntryError>; /** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */ function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; } const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => { const apiRef = useGridApiRef(); const formProps = useForm({ defaultValues: {}, }); const { setValue } = formProps; 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 [searchAllDos, setSearchAllDos] = useState([]); const [totalCount, setTotalCount] = useState(0); const [isWorkbench, setIsWorkbench] = useState(false); const [activeTab, setActiveTab] = useState("2F"); const [searchBoxResetKey, setSearchBoxResetKey] = useState(0); const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10, }); const [currentSearchParams, setCurrentSearchParams] = useState({ code: "", status: "", estimatedArrivalDate: "", orderDate: "", supplierName: "", shopName: "", deliveryOrderLines: "", truckLanceCode: "", codeTo: "", statusTo: "", estimatedArrivalDateTo: "", orderDateTo: "", supplierNameTo: "", shopNameTo: "", deliveryOrderLinesTo: "", truckLanceCodeTo: "", }); const createClearedSearchParams = useCallback( (source: SearchBoxInputs): SearchBoxInputs => ({ code: "", status: "", estimatedArrivalDate: source.estimatedArrivalDate || "", orderDate: "", supplierName: "", shopName: "", deliveryOrderLines: "", truckLanceCode: "", codeTo: "", statusTo: "", estimatedArrivalDateTo: source.estimatedArrivalDateTo || "", orderDateTo: "", supplierNameTo: "", shopNameTo: "", deliveryOrderLinesTo: "", truckLanceCodeTo: "", }), [], ); const [hasSearched, setHasSearched] = useState(false); const [hasResults, setHasResults] = useState(false); const { rowSelectionModel, applyRowSelectionChange, resetSelection, resolveIdsForBatchRelease, } = useDoSearchRowSelection(searchAllDos, setValue); // 当搜索条件变化时,重置到第一页 useEffect(() => { setPagingController(p => ({ ...p, pageNum: 1, })); }, [ currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate, currentSearchParams.truckLanceCode, ]); const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Shop Name"), paramName: "shopName", type: "text" }, { label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" }, { label: t("Estimated Arrival"), paramName: "estimatedArrivalDate", type: "date", preFilledValue: currentSearchParams.estimatedArrivalDate || "", }, { label: t("Status"), paramName: "status", type: "autocomplete", options:[ {label: t('Pending'), value: 'pending'}, {label: t('Receiving'), value: 'receiving'}, {label: t('Completed'), value: 'completed'} ] } ], [t, currentSearchParams.estimatedArrivalDate], ); const onReset = useCallback(async () => { try { setSearchAllDos([]); setTotalCount(0); setHasSearched(false); setHasResults(false); resetSelection(); setPagingController({ pageNum: 1, pageSize: 10 }); } catch (error) { console.error("Error: ", error); setSearchAllDos([]); setTotalCount(0); } }, [resetSelection]); 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: "truckLanceCode", headerName: t("Truck Lance Code"), flex: 1, renderCell: (params) => { const v = params.row.truckLanceCode; if (v == null) return "車線-X"; if (typeof v === "string" && v.trim() === "") return "車線-X"; return v; } }, { 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(params.row.status); }, }, ], [t, arrayToDateString, onDetailClick], ); const onSubmit = useCallback>( async (data, event) => { const hasErrors = false; console.log(errors); }, [errors], ); const onSubmitError = useCallback>( (errors) => {}, [], ); const resolveTabFilter = useCallback((tab: DoSearchTab): TabFilter => { switch (tab) { case "2F": return { floor: "2F", isExtra: false }; case "4F": return { floor: "4F", isExtra: false }; case "TRUCK_X": return { floor: null, isExtra: false, forceTruckKeyword: "x" }; case "ETRA": default: return { floor: null, isExtra: true }; } }, []); const performSearch = useCallback( async ( query: SearchBoxInputs, pageNum: number, pageSize: number, options?: { resetExcludedRows?: boolean; markSearched?: boolean; tabOverride?: DoSearchTab }, ) => { const effectiveTab = options?.tabOverride ?? activeTab; const tabFilter = resolveTabFilter(effectiveTab); const tabTruckKeyword = tabFilter.forceTruckKeyword ?? ""; const effectiveTruckLanceCode = tabTruckKeyword || query.truckLanceCode || ""; const shouldValidateTruckLane = effectiveTab !== "TRUCK_X"; if ( shouldValidateTruckLane && isTruckLaneSearchMissingEta(effectiveTruckLanceCode, query.estimatedArrivalDate ?? "") ) { await Swal.fire({ icon: "warning", title: t("Truck lane search requires date title"), text: t("Truck lane search requires date message"), confirmButtonText: t("Confirm"), }); return false; } let estArrStartDate = query.estimatedArrivalDate; const time = "T00:00:00"; if (estArrStartDate !== "") { estArrStartDate = `${query.estimatedArrivalDate}${time}`; } const status = query.status === "All" ? "" : query.status; const response = await fetchDoSearch( query.code || "", query.shopName || "", status, "", "", estArrStartDate, "", pageNum, pageSize, effectiveTruckLanceCode, tabFilter.floor, tabFilter.isExtra, ); setSearchAllDos(response.records); setTotalCount(response.total); if (options?.markSearched ?? false) { setHasSearched(true); setHasResults(response.records.length > 0); } if (options?.resetExcludedRows ?? false) { resetSelection(); } return true; }, [activeTab, resolveTabFilter, t, resetSelection], ); //SEARCH FUNCTION const handleSearch = useCallback( async (query: SearchBoxInputs) => { try { setCurrentSearchParams(query); await performSearch(query, pagingController.pageNum, pagingController.pageSize, { resetExcludedRows: true, markSearched: true, }); } catch (error) { console.error("Error: ", error); setSearchAllDos([]); setTotalCount(0); setHasSearched(true); setHasResults(false); resetSelection(); } }, [pagingController.pageNum, pagingController.pageSize, performSearch, resetSelection], ); 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) { void performSearch( currentSearchParams, newPagingController.pageNum, newPagingController.pageSize, ).catch((error) => { console.error("Error: ", error); }); } }, [pagingController, hasSearched, currentSearchParams, performSearch], ); const handlePageSizeChange = useCallback( (event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); const newPagingController = { pageNum: 1, pageSize: newPageSize, }; setPagingController(newPagingController); if (hasSearched && currentSearchParams) { void performSearch(currentSearchParams, 1, newPageSize).catch((error) => { console.error("Error: ", error); }); } }, [hasSearched, currentSearchParams, performSearch], ); const handleBatchRelease = useCallback(async (isWorkbench: boolean) => { try { const tabFilter = resolveTabFilter(activeTab); const tabTruckKeyword = tabFilter.forceTruckKeyword ?? ""; const effectiveTruckLanceCode = tabTruckKeyword || currentSearchParams.truckLanceCode || ""; const shouldValidateTruckLane = activeTab !== "TRUCK_X"; if ( shouldValidateTruckLane && isTruckLaneSearchMissingEta( effectiveTruckLanceCode, currentSearchParams.estimatedArrivalDate ?? "", ) ) { await Swal.fire({ icon: "warning", title: t("Truck lane search requires date title"), text: t("Truck lane search requires date message"), confirmButtonText: t("Confirm"), }); return; } // 根据当前搜索条件获取所有匹配的记录(不分页) 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, effectiveTruckLanceCode, tabFilter.floor, tabFilter.isExtra, ); 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 idsToRelease = resolveIdsForBatchRelease( allMatchingDos.map((d) => d.id), ); if (idsToRelease.length === 0) { await Swal.fire({ icon: "warning", title: t("No Records"), text: t("No delivery orders selected for batch release. Uncheck orders you want to exclude, or search again to reset selection."), confirmButtonText: t("OK"), }); return; } // 显示确认对话框 const result = await Swal.fire({ icon: "question", title: t("Batch Release"), html: `

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

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

`, showCancelButton: true, confirmButtonText: t("Confirm"), cancelButtonText: t("Cancel"), confirmButtonColor: "#8dba00", cancelButtonColor: "#F04438" }); if (result.isConfirmed) { try { let startRes ; if(isWorkbench){ startRes = await startWorkbenchBatchReleaseAsyncV2({ ids: idsToRelease, userId: currentUserId ?? 1 }); } else{ startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 }); } //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 = isWorkbench ? await getWorkbenchBatchReleaseProgress(jobId) : 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, resolveIdsForBatchRelease, activeTab, resolveTabFilter]); const handleTabChange = useCallback( (_: React.SyntheticEvent, nextTab: DoSearchTab) => { if (nextTab === activeTab) return; const nextSearchParams = createClearedSearchParams(currentSearchParams); setActiveTab(nextTab); setCurrentSearchParams(nextSearchParams); setSearchBoxResetKey((prev) => prev + 1); setPagingController((prev) => ({ ...prev, pageNum: 1 })); resetSelection(); // 切換 tab 僅重置搜索條件與結果;由使用者再次按「搜索」後才查詢。 setSearchAllDos([]); setTotalCount(0); setHasSearched(false); setHasResults(false); }, [ activeTab, currentSearchParams, createClearedSearchParams, resetSelection, ], ); return ( <> {hasSearched && hasResults && ( )} ); }; const FooterToolbar: React.FC = ({ child }) => { return {child}; }; const NoRowsOverlay: React.FC = () => { const { t } = useTranslation("home"); return ( {t("Add some entries!")} ); }; export default DoSearch;