diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx index 35218db..945128a 100644 --- a/src/components/DoSearch/DoSearch.tsx +++ b/src/components/DoSearch/DoSearch.tsx @@ -33,10 +33,10 @@ import { } from "react-hook-form"; import { Box, Button, Paper, Stack, Tab, Tabs, TablePagination, Typography } 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"; +import { useDoSearchRowSelection } from "./useDoSearchRowSelection"; type Props = { filterArgs?: Record; @@ -79,8 +79,6 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea //console.log("🔍 DoSearch - session:", session); //console.log("🔍 DoSearch - currentUserId:", currentUserId); const [searchTimeout, setSearchTimeout] = useState(null); - /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜索結果視為「已選」以便跨頁記憶 */ - const [excludedRowIds, setExcludedRowIds] = useState([]); const [searchAllDos, setSearchAllDos] = useState([]); const [totalCount, setTotalCount] = useState(0); @@ -135,36 +133,12 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea const [hasSearched, setHasSearched] = useState(false); const [hasResults, setHasResults] = useState(false); - const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); - - const rowSelectionModel = useMemo(() => { - return searchAllDos - .map((r) => r.id) - .filter((id) => !excludedIdSet.has(id)); - }, [searchAllDos, excludedIdSet]); - - const applyRowSelectionChange = useCallback( - (newModel: GridRowSelectionModel) => { - const pageIds = searchAllDos.map((r) => r.id); - const selectedSet = new Set( - newModel.map((id) => (typeof id === "string" ? Number(id) : id)), - ); - setExcludedRowIds((prev) => { - const next = new Set(prev); - for (const id of pageIds) { - next.delete(id); - } - for (const id of pageIds) { - if (!selectedSet.has(id)) { - next.add(id); - } - } - return Array.from(next); - }); - setValue("ids", newModel); - }, - [searchAllDos, setValue], - ); + const { + rowSelectionModel, + applyRowSelectionChange, + resetSelection, + resolveIdsForBatchRelease, + } = useDoSearchRowSelection(searchAllDos, setValue); // 当搜索条件变化时,重置到第一页 useEffect(() => { @@ -212,7 +186,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea setTotalCount(0); setHasSearched(false); setHasResults(false); - setExcludedRowIds([]); + resetSelection(); setPagingController({ pageNum: 1, pageSize: 10 }); } catch (error) { @@ -220,7 +194,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea setSearchAllDos([]); setTotalCount(0); } - }, []); + }, [resetSelection]); const onDetailClick = useCallback( (doResult: DoResult) => { @@ -400,11 +374,11 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea setHasResults(response.records.length > 0); } if (options?.resetExcludedRows ?? false) { - setExcludedRowIds([]); + resetSelection(); } return true; }, - [activeTab, resolveTabFilter, t], + [activeTab, resolveTabFilter, t, resetSelection], ); //SEARCH FUNCTION @@ -422,10 +396,10 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea setTotalCount(0); setHasSearched(true); setHasResults(false); - setExcludedRowIds([]); + resetSelection(); } }, - [pagingController.pageNum, pagingController.pageSize, performSearch], + [pagingController.pageNum, pagingController.pageSize, performSearch, resetSelection], ); useEffect(() => { @@ -584,9 +558,9 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea return; } - const idsToRelease = allMatchingDos - .map((d) => d.id) - .filter((id) => !excludedIdSet.has(id)); + const idsToRelease = resolveIdsForBatchRelease( + allMatchingDos.map((d) => d.id), + ); if (idsToRelease.length === 0) { await Swal.fire({ @@ -705,7 +679,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea confirmButtonText: t("OK") }); } - }, [t, currentUserId, currentSearchParams, handleSearch, excludedIdSet, activeTab, resolveTabFilter]); + }, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease, activeTab, resolveTabFilter]); const handleTabChange = useCallback( (_: React.SyntheticEvent, nextTab: DoSearchTab) => { @@ -715,7 +689,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea setCurrentSearchParams(nextSearchParams); setSearchBoxResetKey((prev) => prev + 1); setPagingController((prev) => ({ ...prev, pageNum: 1 })); - setExcludedRowIds([]); + resetSelection(); // 切換 tab 僅重置搜索條件與結果;由使用者再次按「搜索」後才查詢。 setSearchAllDos([]); setTotalCount(0); @@ -726,6 +700,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea activeTab, currentSearchParams, createClearedSearchParams, + resetSelection, ], ); diff --git a/src/components/DoSearch/useDoSearchRowSelection.ts b/src/components/DoSearch/useDoSearchRowSelection.ts new file mode 100644 index 0000000..974c590 --- /dev/null +++ b/src/components/DoSearch/useDoSearchRowSelection.ts @@ -0,0 +1,139 @@ +import { useCallback, useMemo, useState } from "react"; +import { GridRowSelectionModel } from "@mui/x-data-grid"; + +/** all = 搜索結果默認全選(排除列表 opt-out);none = 全不選;include = 從全不選起逐項勾選;exclude = 從全選起逐項取消 */ +export type DoSelectionMode = "all" | "none" | "include" | "exclude"; + +type RowWithId = { id: number }; + +export function useDoSearchRowSelection( + searchAllDos: RowWithId[], + setValue: (name: "ids", value: GridRowSelectionModel) => void, +) { + const [selectionMode, setSelectionMode] = useState("all"); + const [excludedRowIds, setExcludedRowIds] = useState([]); + const [includedRowIds, setIncludedRowIds] = useState([]); + + const resetSelection = useCallback(() => { + setSelectionMode("all"); + setExcludedRowIds([]); + setIncludedRowIds([]); + }, []); + + const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); + const includedIdSet = useMemo(() => new Set(includedRowIds), [includedRowIds]); + + const rowSelectionModel = useMemo(() => { + const pageIds = searchAllDos.map((r) => r.id); + if (selectionMode === "none") { + return []; + } + if (selectionMode === "include") { + return pageIds.filter((id) => includedIdSet.has(id)); + } + return pageIds.filter((id) => !excludedIdSet.has(id)); + }, [selectionMode, searchAllDos, excludedIdSet, includedIdSet]); + + const applyRowSelectionChange = useCallback( + (newModel: GridRowSelectionModel) => { + const pageIds = searchAllDos.map((r) => r.id); + const selectedSet = new Set( + newModel.map((id) => (typeof id === "string" ? Number(id) : id)), + ); + + const allPageWasSelected = + selectionMode !== "none" && + selectionMode !== "include" && + pageIds.length > 0 && + pageIds.every((id) => !excludedIdSet.has(id)); + + const allPageWasSelectedInIncludeMode = + selectionMode === "include" && + pageIds.length > 0 && + pageIds.every((id) => includedIdSet.has(id)); + + const allPageNowSelected = + pageIds.length > 0 && pageIds.every((id) => selectedSet.has(id)); + + if ( + newModel.length === 0 && + (allPageWasSelected || allPageWasSelectedInIncludeMode) + ) { + setSelectionMode("none"); + setExcludedRowIds([]); + setIncludedRowIds([]); + setValue("ids", []); + return; + } + + if (allPageNowSelected) { + setSelectionMode("all"); + setExcludedRowIds([]); + setIncludedRowIds([]); + setValue("ids", newModel); + return; + } + + if (selectionMode === "all" || selectionMode === "exclude") { + setSelectionMode("exclude"); + setExcludedRowIds((prev) => { + const next = new Set(prev); + for (const id of pageIds) { + next.delete(id); + } + for (const id of pageIds) { + if (!selectedSet.has(id)) { + next.add(id); + } + } + return Array.from(next); + }); + } else { + setSelectionMode("include"); + setIncludedRowIds((prev) => { + const next = new Set(prev); + for (const id of pageIds) { + next.delete(id); + } + for (const id of pageIds) { + if (selectedSet.has(id)) { + next.add(id); + } + } + return Array.from(next); + }); + } + setValue("ids", newModel); + }, + [ + searchAllDos, + setValue, + selectionMode, + excludedIdSet, + includedIdSet, + ], + ); + + const resolveIdsForBatchRelease = useCallback( + (allMatchingIds: number[]) => { + if (selectionMode === "none") { + return []; + } + if (selectionMode === "include") { + return allMatchingIds.filter((id) => includedIdSet.has(id)); + } + return allMatchingIds.filter((id) => !excludedIdSet.has(id)); + }, + [selectionMode, excludedIdSet, includedIdSet], + ); + + return { + selectionMode, + excludedRowIds, + excludedIdSet, + rowSelectionModel, + applyRowSelectionChange, + resetSelection, + resolveIdsForBatchRelease, + }; +} diff --git a/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx b/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx index 8a520ed..8f979b2 100644 --- a/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx +++ b/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx @@ -33,10 +33,10 @@ import { } from "react-hook-form"; import { Box, Button, Paper, 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"; +import { useDoSearchRowSelection } from "../DoSearch/useDoSearchRowSelection"; type Props = { filterArgs?: Record; @@ -83,8 +83,6 @@ const DoSearchWorkbench: React.FC = ({ //console.log("🔍 DoSearch - session:", session); //console.log("🔍 DoSearch - currentUserId:", currentUserId); const [searchTimeout, setSearchTimeout] = useState(null); - /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜索結果視為「已選」以便跨頁記憶 */ - const [excludedRowIds, setExcludedRowIds] = useState([]); const [searchAllDos, setSearchAllDos] = useState([]); const [totalCount, setTotalCount] = useState(0); @@ -118,36 +116,12 @@ const DoSearchWorkbench: React.FC = ({ const [hasSearched, setHasSearched] = useState(false); const [hasResults, setHasResults] = useState(false); - const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); - - const rowSelectionModel = useMemo(() => { - return searchAllDos - .map((r) => r.id) - .filter((id) => !excludedIdSet.has(id)); - }, [searchAllDos, excludedIdSet]); - - const applyRowSelectionChange = useCallback( - (newModel: GridRowSelectionModel) => { - const pageIds = searchAllDos.map((r) => r.id); - const selectedSet = new Set( - newModel.map((id) => (typeof id === "string" ? Number(id) : id)), - ); - setExcludedRowIds((prev) => { - const next = new Set(prev); - for (const id of pageIds) { - next.delete(id); - } - for (const id of pageIds) { - if (!selectedSet.has(id)) { - next.add(id); - } - } - return Array.from(next); - }); - setValue("ids", newModel); - }, - [searchAllDos, setValue], - ); + const { + rowSelectionModel, + applyRowSelectionChange, + resetSelection, + resolveIdsForBatchRelease, + } = useDoSearchRowSelection(searchAllDos, setValue); // 当搜索条件变化时,重置到第一页 useEffect(() => { @@ -204,7 +178,7 @@ const DoSearchWorkbench: React.FC = ({ setTotalCount(0); setHasSearched(false); setHasResults(false); - setExcludedRowIds([]); + resetSelection(); setPagingController({ pageNum: 1, pageSize: 10 }); } catch (error) { @@ -212,7 +186,7 @@ const DoSearchWorkbench: React.FC = ({ setSearchAllDos([]); setTotalCount(0); } - }, []); + }, [resetSelection]); const onDetailClick = useCallback( (doResult: DoResult) => { @@ -367,7 +341,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { setTotalCount(response.total); // 设置总记录数 setHasSearched(true); setHasResults(response.records.length > 0); - setExcludedRowIds([]); + resetSelection(); } catch (error) { console.error("Error: ", error); @@ -375,9 +349,9 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { setTotalCount(0); setHasSearched(true); setHasResults(false); - setExcludedRowIds([]); + resetSelection(); } -}, [pagingController, t]); +}, [pagingController, t, resetSelection]); useEffect(() => { if (typeof window !== 'undefined') { @@ -632,9 +606,9 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { return; } - const idsToRelease = allMatchingDos - .map((d) => d.id) - .filter((id) => !excludedIdSet.has(id)); + const idsToRelease = resolveIdsForBatchRelease( + allMatchingDos.map((d) => d.id), + ); if (idsToRelease.length === 0) { await Swal.fire({ @@ -749,7 +723,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { confirmButtonText: t("OK") }); } - }, [t, currentUserId, currentSearchParams, handleSearch, excludedIdSet]); + }, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease]); return ( <>