| @@ -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<string, any>; | |||
| @@ -79,8 +79,6 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| //console.log("🔍 DoSearch - session:", session); | |||
| //console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | |||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜索結果視為「已選」以便跨頁記憶 */ | |||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||
| const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | |||
| const [totalCount, setTotalCount] = useState(0); | |||
| @@ -135,36 +133,12 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| const [hasSearched, setHasSearched] = useState(false); | |||
| const [hasResults, setHasResults] = useState(false); | |||
| const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); | |||
| const rowSelectionModel = useMemo<GridRowSelectionModel>(() => { | |||
| 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<Props> = ({ 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<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| setSearchAllDos([]); | |||
| setTotalCount(0); | |||
| } | |||
| }, []); | |||
| }, [resetSelection]); | |||
| const onDetailClick = useCallback( | |||
| (doResult: DoResult) => { | |||
| @@ -400,11 +374,11 @@ const DoSearch: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| activeTab, | |||
| currentSearchParams, | |||
| createClearedSearchParams, | |||
| resetSelection, | |||
| ], | |||
| ); | |||
| @@ -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<DoSelectionMode>("all"); | |||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||
| const [includedRowIds, setIncludedRowIds] = useState<number[]>([]); | |||
| const resetSelection = useCallback(() => { | |||
| setSelectionMode("all"); | |||
| setExcludedRowIds([]); | |||
| setIncludedRowIds([]); | |||
| }, []); | |||
| const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); | |||
| const includedIdSet = useMemo(() => new Set(includedRowIds), [includedRowIds]); | |||
| const rowSelectionModel = useMemo<GridRowSelectionModel>(() => { | |||
| 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, | |||
| }; | |||
| } | |||
| @@ -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<string, any>; | |||
| @@ -83,8 +83,6 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| //console.log("🔍 DoSearch - session:", session); | |||
| //console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | |||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜索結果視為「已選」以便跨頁記憶 */ | |||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||
| const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | |||
| const [totalCount, setTotalCount] = useState(0); | |||
| @@ -118,36 +116,12 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| const [hasSearched, setHasSearched] = useState(false); | |||
| const [hasResults, setHasResults] = useState(false); | |||
| const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); | |||
| const rowSelectionModel = useMemo<GridRowSelectionModel>(() => { | |||
| 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<Props> = ({ | |||
| setTotalCount(0); | |||
| setHasSearched(false); | |||
| setHasResults(false); | |||
| setExcludedRowIds([]); | |||
| resetSelection(); | |||
| setPagingController({ pageNum: 1, pageSize: 10 }); | |||
| } | |||
| catch (error) { | |||
| @@ -212,7 +186,7 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| 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 ( | |||
| <> | |||