From 1407d93704d8311251c1f24f6fd9d3e2d1ed33d6 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 22 Nov 2025 15:14:19 +0330 Subject: [PATCH] feat(tenders): Add filtering functionality to tender list table - Create new TenderListFilters component with title, status, and country code filter fields - Add filter modal to TendersTable component with open/close state management - Implement useForm hook in useTenderListPresenter for filter form handling - Add ListHeader component with filter button to tender table - Replace debounced search with URL search params-based filtering - Integrate filter form submission with tender query parameters - Add isMutating state tracking for loading indicator during filter operations - Remove deprecated useDebounce hook and search state from presenter - Improve filter UX with modal dialog and clear button functionality --- .../Tables/tenders/TenderListFilters.tsx | 89 +++++++++++++++++++ src/components/Tables/tenders/index.tsx | 41 ++++++++- .../Tables/tenders/useTenderListPresenter.ts | 61 +++++++++---- 3 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 src/components/Tables/tenders/TenderListFilters.tsx diff --git a/src/components/Tables/tenders/TenderListFilters.tsx b/src/components/Tables/tenders/TenderListFilters.tsx new file mode 100644 index 0000000..f3620fe --- /dev/null +++ b/src/components/Tables/tenders/TenderListFilters.tsx @@ -0,0 +1,89 @@ +import InputGroup from "@/components/FormElements/InputGroup"; +import { Select } from "@/components/FormElements/select"; +import { Countries } from "@/constants/countries"; +import { TenderStatus } from "@/constants/enums"; +import { getEnumAsArray } from "@/utils/shared"; +import { + FieldValues, + UseFormHandleSubmit, + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from "react-hook-form"; + +interface TenderListFiltersProps { + filterRegister: UseFormRegister; + setIsFilterModalOpen: (isOpen: boolean) => void; + isMutating: number; + handleFilterSubmit: UseFormHandleSubmit; + search: (data: any) => void; + watch: UseFormWatch; + setValue: UseFormSetValue; +} + +const TenderListFilters = ({ + filterRegister, + setIsFilterModalOpen, + isMutating, + handleFilterSubmit, + search, + watch, + setValue, +}: TenderListFiltersProps) => { + return ( +
+ + setValue("country_code", undefined)} + /> + +
+ + +
+ + ); +}; + +export default TenderListFilters; diff --git a/src/components/Tables/tenders/index.tsx b/src/components/Tables/tenders/index.tsx index a89295b..d0e15b3 100644 --- a/src/components/Tables/tenders/index.tsx +++ b/src/components/Tables/tenders/index.tsx @@ -13,17 +13,38 @@ import { Tooltip } from "react-tooltip"; import useTenderListPresenter from "./useTenderListPresenter"; import { ExclamationIcon, EyeIcon } from "@/assets/icons"; +import ListHeader from "@/components/ui/ListHeader"; +import Modal from "@/components/ui/modal"; import Pagination from "@/components/ui/pagination"; import TableSkeleton from "@/components/ui/TableSkeleton"; import { _TooltipDefaultParams } from "@/constants/tooltip"; import { cn } from "@/lib/utils"; import Link from "next/link"; +import TenderListFilters from "./TenderListFilters"; const TendersTable = () => { - const { allTenders, isPending, router, pathName, setParams, columns } = - useTenderListPresenter(); + const { + allTenders, + isPending, + router, + pathName, + setParams, + columns, + isFilterModalOpen, + setIsFilterModalOpen, + filterRegister, + handleFilterSubmit, + search, + watch, + setFilterValue, + isMutating, + } = useTenderListPresenter(); return (
+ setIsFilterModalOpen(true)} + createButtonText="Create Tender" + /> @@ -104,6 +125,22 @@ const TendersTable = () => { )}
+ setIsFilterModalOpen(false)} + classNames="w-full xl:w-1/2" + showButtons={false} + > + + { const [isModalOpen, setIsModalOpen] = useState(false); + const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [currentTender, setCurrentTender] = useState(null); - const [search, setSearch] = useState(""); const [modalConfig, setModalConfig] = useState< Partial >({}); - const [params, setParams] = useState>({ - ...apiDefaultParams, - }); + const [params, setParams] = useState>({}); const router = useRouter(); - const debouncedSearch = useDebounce(search, 1000); const pathName = usePathname(); - const { data, error, isPending } = useTendersQuery(params); + const searchParams = useSearchParams(); + const isMutating = useIsMutating(); + + const { + register: filterRegister, + handleSubmit: handleFilterSubmit, + reset: filterFormReset, + watch, + setValue: setFilterValue, + } = useForm(); useEffect(() => { - const result = z.string().safeParse(debouncedSearch); - if (result.success) { - setParams((prevParams) => ({ - ...prevParams, - })); + const newParams: Record = { + ...apiDefaultParams, + offset: 0, + limit: 10, + }; + for (const [key, value] of searchParams.entries()) { + newParams[key] = value; } - }, [debouncedSearch]); + setParams(newParams); + filterFormReset(newParams); + }, [searchParams, filterFormReset]); - const handleFilterChange = (e: React.ChangeEvent) => { - setSearch(e.target.value); + const { data, error, isPending } = useTendersQuery(params); + + const search = (data: any) => { + const newParams = { ...params, ...data, offset: 0 }; + const cleanedParams = deleteEmptyKeys(newParams); + const queryString = new URLSearchParams(cleanedParams).toString(); + router.push(`${pathName}?${queryString}`); + setIsFilterModalOpen(false); }; const columns = [ "row", @@ -46,8 +63,6 @@ const useTenderListPresenter = () => { currentTender, columns, setCurrentTender, - search, - handleFilterChange, router, allTenders: data, error, @@ -59,6 +74,14 @@ const useTenderListPresenter = () => { setIsModalOpen, modalConfig, setModalConfig, + isFilterModalOpen, + setIsFilterModalOpen, + filterRegister, + handleFilterSubmit, + search, + watch, + setFilterValue, + isMutating, }; };