feat(admins): implement admin status management
This commit introduces the functionality for super admins to change the status of other admin users to either 'Active' or 'Inactive'. This provides better control over admin account access. Key changes include: - A 'Status' column with a visual badge has been added to the admins table. - A new 'Change Status' action button, using a new `UserSettingIcon`, opens a modal for status updates. - The backend service and frontend mutation for updating an admin's status are implemented. - The `IUser` type and a new `AdminStatus` enum have been added to support this feature. Additionally, the confirmation modal implementation has been simplified across the admin and customer tables by removing the `modalConfig` state.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { AdminStatus } from "@/constants/enums";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { TChangeAdminStatusCredentials } from "@/lib/api";
|
||||
import { capitalize } from "@/utils/shared";
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
interface IProps {
|
||||
status: AdminStatus;
|
||||
setStatus: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
const ChangeStatusModalContent: FC<IProps> = ({ status, setStatus }) => {
|
||||
const { register } = useForm<TChangeAdminStatusCredentials>({
|
||||
mode: "onChange",
|
||||
defaultValues: { status },
|
||||
});
|
||||
const adminStatusEntries = Object.entries(AdminStatus).map(
|
||||
([key, value]) => ({
|
||||
label: capitalize(key),
|
||||
value,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<Select
|
||||
{...register("status", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
onChange(event) {
|
||||
setStatus(event.target.value);
|
||||
},
|
||||
})}
|
||||
items={adminStatusEntries}
|
||||
label="Status"
|
||||
name="status"
|
||||
defaultValue={status}
|
||||
placeholder="Please select new Status"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeStatusModalContent;
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import Status from "@/components/ui/Status";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -11,7 +13,9 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { AdminStatus } from "@/constants/enums";
|
||||
import Link from "next/link";
|
||||
import ChangeStatusModalContent from "./ChangeStatusModalContent";
|
||||
import { useAdminsPresenter } from "./useAdminsPresenter";
|
||||
|
||||
const AdminsTable = () => {
|
||||
@@ -22,9 +26,15 @@ const AdminsTable = () => {
|
||||
onConfirmDelete,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
modalConfig,
|
||||
isChangeStatusModalOpen,
|
||||
setIsChangeStatusModalOpen,
|
||||
columns,
|
||||
setCurrentUser,
|
||||
pathName,
|
||||
changeStatus,
|
||||
currentUser,
|
||||
setStatus,
|
||||
status,
|
||||
} = useAdminsPresenter();
|
||||
|
||||
return (
|
||||
@@ -35,31 +45,24 @@ const AdminsTable = () => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Full name
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Email
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Role
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Username
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Actions
|
||||
</TableHead>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column} colSpan={100}>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{isPending && <TableSkeleton column={5} />}
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{allUsers.map((item) => {
|
||||
{allUsers.map((item, index) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.full_name}
|
||||
</TableCell>
|
||||
@@ -72,6 +75,9 @@ const AdminsTable = () => {
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.username}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<Status status={item.status ?? ""}>{item.status}</Status>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
@@ -85,6 +91,16 @@ const AdminsTable = () => {
|
||||
<span className="sr-only">Delete Admin </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
className="hover:text-yellow-dark"
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsChangeStatusModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Change Admin Status</span>
|
||||
<UserSettingIcon />
|
||||
</button>
|
||||
<button
|
||||
className="hover:text-primary"
|
||||
onClick={() => router.push(`/admins/edit/${item.id}`)}
|
||||
@@ -99,15 +115,27 @@ const AdminsTable = () => {
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={isChangeStatusModalOpen}
|
||||
onClose={() => setIsChangeStatusModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
changeStatus({ id: currentUser?.id!, credentials: { status } });
|
||||
}}
|
||||
>
|
||||
<ChangeStatusModalContent
|
||||
status={currentUser?.status as AdminStatus}
|
||||
setStatus={setStatus}
|
||||
/>
|
||||
</Modal>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
title={modalConfig.title || "Confirm"}
|
||||
message={modalConfig.message || "Are you sure?"}
|
||||
variant={modalConfig.variant || "warning"}
|
||||
size={modalConfig.size || "md"}
|
||||
confirmText={modalConfig.confirmText ?? "Confirm"}
|
||||
cancelText={modalConfig.cancelText ?? "Cancel"}
|
||||
title={"Confirm"}
|
||||
message={"Are you sure?"}
|
||||
variant={"warning"}
|
||||
size={"md"}
|
||||
confirmText={"Confirm"}
|
||||
cancelText={"Cancel"}
|
||||
onConfirm={() => {
|
||||
onConfirmDelete();
|
||||
}}
|
||||
|
||||
@@ -1,84 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useAdminListInfiniteQuery, useDeleteAdmin } from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { IUser } from "@/lib/api";
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import { apiDefaultParams } from "@/constants/Api_Params";
|
||||
import { AdminStatus } from "@/constants/enums";
|
||||
import {
|
||||
useAdminListInfiniteQuery,
|
||||
useChangeAdminStatus,
|
||||
useDeleteAdmin,
|
||||
} from "@/hooks/queries";
|
||||
import { IUser, TChangeAdminStatusCredentials } from "@/lib/api";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const useAdminsPresenter = () => {
|
||||
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<IUser | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [status, setStatus] = useState<string>(
|
||||
currentUser?.status ?? AdminStatus.ACTIVE,
|
||||
);
|
||||
const router = useRouter();
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
Partial<ConfirmationModalProps>
|
||||
>({});
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
const pathName = usePathname();
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useAdminListInfiniteQuery(params);
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useForm<TChangeAdminStatusCredentials>({
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
status: currentUser?.status,
|
||||
},
|
||||
});
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
...apiDefaultParams,
|
||||
});
|
||||
const pathName = usePathname();
|
||||
const { data, isPending } = useAdminListInfiniteQuery(params);
|
||||
const { mutate: deleteAdmin } = useDeleteAdmin(() => {
|
||||
setIsModalOpen(false);
|
||||
});
|
||||
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const result = z.string().safeParse(debouncedSearch);
|
||||
if (result.success) {
|
||||
setParams((prevParams) => ({
|
||||
...prevParams,
|
||||
}));
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
const onConfirmDelete = () => {
|
||||
if (currentUser) {
|
||||
deleteAdmin(currentUser.id);
|
||||
}
|
||||
};
|
||||
const { mutate: changeStatus } = useChangeAdminStatus({
|
||||
id: currentUser?.id ?? "",
|
||||
successCallback: () => {
|
||||
setIsChangeStatusModalOpen(false);
|
||||
},
|
||||
});
|
||||
const allUsers = data?.pages.flatMap((page) => page.data.users) ?? [];
|
||||
const columns = [
|
||||
"row",
|
||||
"full name",
|
||||
"email",
|
||||
"role",
|
||||
"username",
|
||||
"status",
|
||||
"actions",
|
||||
];
|
||||
return {
|
||||
allUsers,
|
||||
currentAdmin: currentUser,
|
||||
error,
|
||||
handleFilterChange,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
columns,
|
||||
isPending,
|
||||
onConfirmDelete,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
search,
|
||||
hasNextPage,
|
||||
data,
|
||||
router,
|
||||
setCurrentUser,
|
||||
pathName,
|
||||
status,
|
||||
setStatus,
|
||||
changeStatus,
|
||||
isChangeStatusModalOpen,
|
||||
currentUser,
|
||||
setIsChangeStatusModalOpen,
|
||||
register,
|
||||
errors,
|
||||
handleSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user