feat(admins): add filter functionality to admins list

This commit introduces filtering capabilities to the administrators table, allowing users to refine the list based on specific criteria.

A new `ListHeader` component has been implemented, which now contains the "Create Admin" button and a new "Filter" button. Clicking the filter button opens a modal containing the `AdminListFilters` form. The state for managing the filter modal's visibility is handled within the `useAdminsPresenter` hook. A new `FilterIcon` has also been added to support this feature.
This commit is contained in:
AmirReza Jamali
2025-10-12 15:20:52 +03:30
parent 1f17a1c601
commit 5b73389d64
11 changed files with 191 additions and 26 deletions
+29
View File
@@ -38,6 +38,35 @@ export function CurrencyIcon(props: IconProps) {
</svg> </svg>
); );
} }
export function FilterIcon(props: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5.40002 2.09998H18.6C19.7 2.09998 20.6 2.99998 20.6 4.09998V6.29998C20.6 7.09998 20.1 8.09998 19.6 8.59998L15.3 12.4C14.7 12.9 14.3 13.9 14.3 14.7V19C14.3 19.6 13.9 20.4 13.4 20.7L12 21.6C10.7 22.4 8.90002 21.5 8.90002 19.9V14.6C8.90002 13.9 8.50002 13 8.10002 12.5L4.30002 8.49998C3.80002 7.99998 3.40002 7.09998 3.40002 6.49998V4.19998C3.40002 2.99998 4.30002 2.09998 5.40002 2.09998Z"
stroke="#9299A6"
strokeWidth="1.5"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.93 2.09998L6 9.99998"
stroke="#9299A6"
strokeWidth="1.5"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function MoneyIcon(props: IconProps) { export function MoneyIcon(props: IconProps) {
return ( return (
<svg <svg
@@ -0,0 +1,59 @@
import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select";
import { AdminRoles, NotificationChannels } from "@/constants/enums";
import { getEnumAsArray } from "@/utils/shared";
import useAdminListFilterPresenter from "./useAdminListFilterPresenter";
interface IProps {}
const AdminListFilters = ({}: IProps) => {
const { register } = useAdminListFilterPresenter();
return (
<form className="grid !min-w-full grid-cols-3 gap-4">
<Select
{...register("method")}
items={getEnumAsArray(NotificationChannels)}
label="Method"
name="method"
placeholder="Method"
/>
<Select
{...register("status")}
items={getEnumAsArray(NotificationChannels)}
label="status"
name="status"
placeholder="Status"
/>
<InputGroup
{...register("email")}
name="email"
label="email"
placeholder="email"
type="email"
/>
<Select
{...register("role")}
name="role"
items={getEnumAsArray(AdminRoles)}
label="Role"
placeholder="Role"
/>
<InputGroup
{...register("username")}
label="username"
name="username"
placeholder="username"
type="text"
/>
<InputGroup
{...register("full_name")}
label="full name"
name="full_name"
placeholder="full name"
type="text"
/>
</form>
);
};
export default AdminListFilters;
+17 -6
View File
@@ -2,8 +2,8 @@
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons"; import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
import { import {
Table, Table,
@@ -15,8 +15,9 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton"; import TableSkeleton from "@/components/ui/TableSkeleton";
import { AdminStatus } from "@/constants/enums"; import { AdminStatus } from "@/constants/enums";
import Link from "next/link"; import { _TooltipDefaultParams } from "@/constants/tooltip";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import AdminListFilters from "./AdminListFilters";
import ChangeStatusModalContent from "./ChangeStatusModalContent"; import ChangeStatusModalContent from "./ChangeStatusModalContent";
import { useAdminsPresenter } from "./useAdminsPresenter"; import { useAdminsPresenter } from "./useAdminsPresenter";
@@ -32,7 +33,8 @@ const AdminsTable = () => {
setIsChangeStatusModalOpen, setIsChangeStatusModalOpen,
columns, columns,
setCurrentUser, setCurrentUser,
pathName, isFilterModalOpen,
setIsFilterModalOpen,
changeStatus, changeStatus,
currentUser, currentUser,
setStatus, setStatus,
@@ -41,9 +43,10 @@ const AdminsTable = () => {
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"> <ListHeader
<Link href={`${pathName}/create`}>Create Admin</Link> onFilter={() => setIsFilterModalOpen(true)}
</button> createButtonText="Create Admin"
/>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-none uppercase [&>th]:text-start"> <TableRow className="border-none uppercase [&>th]:text-start">
@@ -143,6 +146,14 @@ const AdminsTable = () => {
setStatus={setStatus} setStatus={setStatus}
/> />
</Modal> </Modal>
<Modal
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
classNames="w-full"
>
<AdminListFilters />
</Modal>
{isModalOpen && ( {isModalOpen && (
<ConfirmationModal <ConfirmationModal
isOpen={isModalOpen} isOpen={isModalOpen}
@@ -0,0 +1,10 @@
import { useForm } from "react-hook-form";
const useAdminListFilterPresenter = () => {
const { register } = useForm();
return {
register,
};
};
export default useAdminListFilterPresenter;
@@ -14,6 +14,7 @@ import { useForm } from "react-hook-form";
export const useAdminsPresenter = () => { export const useAdminsPresenter = () => {
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false); const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false);
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<IUser | null>(null); const [currentUser, setCurrentUser] = useState<IUser | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [status, setStatus] = useState<string>( const [status, setStatus] = useState<string>(
@@ -79,5 +80,7 @@ export const useAdminsPresenter = () => {
register, register,
errors, errors,
handleSubmit, handleSubmit,
isFilterModalOpen,
setIsFilterModalOpen,
}; };
}; };
@@ -154,6 +154,7 @@ const CustomersTable = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<div className="w-1/12">
<Modal <Modal
onConfirm={() => { onConfirm={() => {
if (!selectedCompanies || !currentCustomer) { if (!selectedCompanies || !currentCustomer) {
@@ -173,6 +174,7 @@ const CustomersTable = () => {
defaultValues={selectedCompanies} defaultValues={selectedCompanies}
/> />
</Modal> </Modal>
</div>
{isModalOpen && ( {isModalOpen && (
<ConfirmationModal <ConfirmationModal
isOpen={isModalOpen} isOpen={isModalOpen}
@@ -66,6 +66,7 @@ const NotificationHistoryTable = () => {
unix: item.created_at, unix: item.created_at,
})} })}
</TableCell> </TableCell>
<TableCell colSpan={100}>{item.recipient?.full_name}</TableCell>
<TableCell colSpan={100} className="truncate"> <TableCell colSpan={100} className="truncate">
{truncateString(item.message, 20, 20)} {truncateString(item.message, 20, 20)}
</TableCell> </TableCell>
@@ -15,6 +15,7 @@ const useNotificationHistoryTablePresenter = () => {
"title", "title",
"channel", "channel",
"created at", "created at",
"receiver",
"message", "message",
"priority", "priority",
"type", "type",
+45
View File
@@ -0,0 +1,45 @@
"use client";
import { FilterIcon } from "@/assets/icons";
import Link from "next/link";
import { usePathname } from "next/navigation";
import IsVisible from "./IsVisible";
interface IProps {
hasCreate?: boolean;
hasFilter?: boolean;
createButtonText?: string;
filterButtonText?: string;
onFilter?: () => void;
}
const ListHeader = ({
hasCreate = true,
hasFilter = true,
createButtonText = "Create",
filterButtonText = "Filter",
onFilter,
}: IProps) => {
const pathName = usePathname();
return (
<div className="flex items-center justify-between">
<IsVisible condition={hasFilter}>
<button
onClick={onFilter}
className="mb-5 flex w-fit justify-center self-end rounded-2xl border bg-gray-5 bg-opacity-20 px-6 py-2 font-medium transition duration-300 hover:bg-opacity-30 dark:border-gray-7 dark:bg-black dark:bg-opacity-40 dark:text-white dark:hover:bg-opacity-90"
>
<span>
<FilterIcon />
</span>
{filterButtonText}
</button>
</IsVisible>
<IsVisible condition={hasCreate}>
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90">
<Link href={`${pathName}/create`}>{createButtonText}</Link>
</button>
</IsVisible>
</div>
);
};
export default ListHeader;
+3 -1
View File
@@ -12,6 +12,7 @@ interface ModalProps {
confirmText?: string; confirmText?: string;
cancelText?: string; cancelText?: string;
showButtons?: boolean; showButtons?: boolean;
classNames?: string;
} }
const Modal: React.FC<ModalProps> = ({ const Modal: React.FC<ModalProps> = ({
@@ -22,6 +23,7 @@ const Modal: React.FC<ModalProps> = ({
confirmText = "confirm", confirmText = "confirm",
cancelText = "close", cancelText = "close",
showButtons = true, showButtons = true,
classNames,
}) => { }) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const isMutating = useIsMutating(); const isMutating = useIsMutating();
@@ -49,7 +51,7 @@ const Modal: React.FC<ModalProps> = ({
onClick={onClose} onClick={onClose}
> >
<div <div
className="max-h-[90vh] min-w-75 max-w-[90vw] overflow-auto rounded-lg bg-white p-5 shadow-4 dark:bg-dark-2" className={`max-h-[90vh] max-w-[90vw] overflow-auto rounded-lg bg-white p-5 shadow-4 dark:bg-dark-2 ${classNames}`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<button <button
@@ -6,6 +6,7 @@ import { toast } from "react-toastify";
export const useGetNotificationHistoryQuery = ( export const useGetNotificationHistoryQuery = (
params?: Record<string, any>, params?: Record<string, any>,
options?: { enabled: boolean },
) => { ) => {
const queryKey = useMemo( const queryKey = useMemo(
() => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, params], () => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, params],
@@ -14,6 +15,7 @@ export const useGetNotificationHistoryQuery = (
return useQuery({ return useQuery({
queryKey, queryKey,
queryFn: () => notificationService.getNotifications(params), queryFn: () => notificationService.getNotifications(params),
...options,
}); });
}; };
export const useGetMyNotifications = (params?: Record<string, any>) => { export const useGetMyNotifications = (params?: Record<string, any>) => {