| @@ -33,10 +33,10 @@ import { | |||||
| } from "react-hook-form"; | } from "react-hook-form"; | ||||
| import { Box, Button, Paper, Stack, Tab, Tabs, TablePagination, Typography } from "@mui/material"; | import { Box, Button, Paper, Stack, Tab, Tabs, TablePagination, Typography } from "@mui/material"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { GridRowSelectionModel } from "@mui/x-data-grid"; | |||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { useDoSearchRowSelection } from "./useDoSearchRowSelection"; | |||||
| type Props = { | type Props = { | ||||
| filterArgs?: Record<string, any>; | filterArgs?: Record<string, any>; | ||||
| @@ -79,8 +79,6 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| //console.log("🔍 DoSearch - session:", session); | //console.log("🔍 DoSearch - session:", session); | ||||
| //console.log("🔍 DoSearch - currentUserId:", currentUserId); | //console.log("🔍 DoSearch - currentUserId:", currentUserId); | ||||
| const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | ||||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜索結果視為「已選」以便跨頁記憶 */ | |||||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||||
| const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | ||||
| const [totalCount, setTotalCount] = useState(0); | const [totalCount, setTotalCount] = useState(0); | ||||
| @@ -135,36 +133,12 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| const [hasSearched, setHasSearched] = useState(false); | const [hasSearched, setHasSearched] = useState(false); | ||||
| const [hasResults, setHasResults] = 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(() => { | useEffect(() => { | ||||
| @@ -212,7 +186,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| setTotalCount(0); | setTotalCount(0); | ||||
| setHasSearched(false); | setHasSearched(false); | ||||
| setHasResults(false); | setHasResults(false); | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| setPagingController({ pageNum: 1, pageSize: 10 }); | setPagingController({ pageNum: 1, pageSize: 10 }); | ||||
| } | } | ||||
| catch (error) { | catch (error) { | ||||
| @@ -220,7 +194,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| setSearchAllDos([]); | setSearchAllDos([]); | ||||
| setTotalCount(0); | setTotalCount(0); | ||||
| } | } | ||||
| }, []); | |||||
| }, [resetSelection]); | |||||
| const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
| (doResult: DoResult) => { | (doResult: DoResult) => { | ||||
| @@ -400,11 +374,11 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| setHasResults(response.records.length > 0); | setHasResults(response.records.length > 0); | ||||
| } | } | ||||
| if (options?.resetExcludedRows ?? false) { | if (options?.resetExcludedRows ?? false) { | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| } | } | ||||
| return true; | return true; | ||||
| }, | }, | ||||
| [activeTab, resolveTabFilter, t], | |||||
| [activeTab, resolveTabFilter, t, resetSelection], | |||||
| ); | ); | ||||
| //SEARCH FUNCTION | //SEARCH FUNCTION | ||||
| @@ -422,10 +396,10 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| setTotalCount(0); | setTotalCount(0); | ||||
| setHasSearched(true); | setHasSearched(true); | ||||
| setHasResults(false); | setHasResults(false); | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| } | } | ||||
| }, | }, | ||||
| [pagingController.pageNum, pagingController.pageSize, performSearch], | |||||
| [pagingController.pageNum, pagingController.pageSize, performSearch, resetSelection], | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -584,9 +558,9 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| return; | 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) { | if (idsToRelease.length === 0) { | ||||
| await Swal.fire({ | await Swal.fire({ | ||||
| @@ -705,7 +679,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| confirmButtonText: t("OK") | confirmButtonText: t("OK") | ||||
| }); | }); | ||||
| } | } | ||||
| }, [t, currentUserId, currentSearchParams, handleSearch, excludedIdSet, activeTab, resolveTabFilter]); | |||||
| }, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease, activeTab, resolveTabFilter]); | |||||
| const handleTabChange = useCallback( | const handleTabChange = useCallback( | ||||
| (_: React.SyntheticEvent, nextTab: DoSearchTab) => { | (_: React.SyntheticEvent, nextTab: DoSearchTab) => { | ||||
| @@ -715,7 +689,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| setCurrentSearchParams(nextSearchParams); | setCurrentSearchParams(nextSearchParams); | ||||
| setSearchBoxResetKey((prev) => prev + 1); | setSearchBoxResetKey((prev) => prev + 1); | ||||
| setPagingController((prev) => ({ ...prev, pageNum: 1 })); | setPagingController((prev) => ({ ...prev, pageNum: 1 })); | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| // 切換 tab 僅重置搜索條件與結果;由使用者再次按「搜索」後才查詢。 | // 切換 tab 僅重置搜索條件與結果;由使用者再次按「搜索」後才查詢。 | ||||
| setSearchAllDos([]); | setSearchAllDos([]); | ||||
| setTotalCount(0); | setTotalCount(0); | ||||
| @@ -726,6 +700,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| activeTab, | activeTab, | ||||
| currentSearchParams, | currentSearchParams, | ||||
| createClearedSearchParams, | 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"; | } from "react-hook-form"; | ||||
| import { Box, Button, Paper, Stack, Typography, TablePagination } from "@mui/material"; | import { Box, Button, Paper, Stack, Typography, TablePagination } from "@mui/material"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { GridRowSelectionModel } from "@mui/x-data-grid"; | |||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { useDoSearchRowSelection } from "../DoSearch/useDoSearchRowSelection"; | |||||
| type Props = { | type Props = { | ||||
| filterArgs?: Record<string, any>; | filterArgs?: Record<string, any>; | ||||
| @@ -83,8 +83,6 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| //console.log("🔍 DoSearch - session:", session); | //console.log("🔍 DoSearch - session:", session); | ||||
| //console.log("🔍 DoSearch - currentUserId:", currentUserId); | //console.log("🔍 DoSearch - currentUserId:", currentUserId); | ||||
| const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | ||||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜索結果視為「已選」以便跨頁記憶 */ | |||||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||||
| const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | ||||
| const [totalCount, setTotalCount] = useState(0); | const [totalCount, setTotalCount] = useState(0); | ||||
| @@ -118,36 +116,12 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| const [hasSearched, setHasSearched] = useState(false); | const [hasSearched, setHasSearched] = useState(false); | ||||
| const [hasResults, setHasResults] = 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(() => { | useEffect(() => { | ||||
| @@ -204,7 +178,7 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| setTotalCount(0); | setTotalCount(0); | ||||
| setHasSearched(false); | setHasSearched(false); | ||||
| setHasResults(false); | setHasResults(false); | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| setPagingController({ pageNum: 1, pageSize: 10 }); | setPagingController({ pageNum: 1, pageSize: 10 }); | ||||
| } | } | ||||
| catch (error) { | catch (error) { | ||||
| @@ -212,7 +186,7 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| setSearchAllDos([]); | setSearchAllDos([]); | ||||
| setTotalCount(0); | setTotalCount(0); | ||||
| } | } | ||||
| }, []); | |||||
| }, [resetSelection]); | |||||
| const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
| (doResult: DoResult) => { | (doResult: DoResult) => { | ||||
| @@ -367,7 +341,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| setTotalCount(response.total); // 设置总记录数 | setTotalCount(response.total); // 设置总记录数 | ||||
| setHasSearched(true); | setHasSearched(true); | ||||
| setHasResults(response.records.length > 0); | setHasResults(response.records.length > 0); | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error: ", error); | console.error("Error: ", error); | ||||
| @@ -375,9 +349,9 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| setTotalCount(0); | setTotalCount(0); | ||||
| setHasSearched(true); | setHasSearched(true); | ||||
| setHasResults(false); | setHasResults(false); | ||||
| setExcludedRowIds([]); | |||||
| resetSelection(); | |||||
| } | } | ||||
| }, [pagingController, t]); | |||||
| }, [pagingController, t, resetSelection]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (typeof window !== 'undefined') { | if (typeof window !== 'undefined') { | ||||
| @@ -632,9 +606,9 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| return; | 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) { | if (idsToRelease.length === 0) { | ||||
| await Swal.fire({ | await Swal.fire({ | ||||
| @@ -749,7 +723,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| confirmButtonText: t("OK") | confirmButtonText: t("OK") | ||||
| }); | }); | ||||
| } | } | ||||
| }, [t, currentUserId, currentSearchParams, handleSearch, excludedIdSet]); | |||||
| }, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||