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:
AmirReza Jamali
2025-09-21 13:18:53 +03:30
parent e00e75b8be
commit 1d78b2b65d
18 changed files with 274 additions and 173 deletions
+14
View File
@@ -215,6 +215,20 @@ export function GlobeIcon(props: IconProps) {
); );
} }
export function UserSettingIcon(props: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="currentColor"
{...props}
>
<path d="M400-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM80-160v-112q0-33 17-62t47-44q51-26 115-44t141-18h14q6 0 12 2-8 18-13.5 37.5T404-360h-4q-71 0-127.5 18T180-306q-9 5-14.5 14t-5.5 20v32h252q6 21 16 41.5t22 38.5H80Zm560 40-12-60q-12-5-22.5-10.5T584-204l-58 18-40-68 46-40q-2-14-2-26t2-26l-46-40 40-68 58 18q11-8 21.5-13.5T628-460l12-60h80l12 60q12 5 22.5 11t21.5 15l58-20 40 70-46 40q2 12 2 25t-2 25l46 40-40 68-58-18q-11 8-21.5 13.5T732-180l-12 60h-80Zm40-120q33 0 56.5-23.5T760-320q0-33-23.5-56.5T680-400q-33 0-56.5 23.5T600-320q0 33 23.5 56.5T680-240ZM400-560q33 0 56.5-23.5T480-640q0-33-23.5-56.5T400-720q-33 0-56.5 23.5T320-640q0 33 23.5 56.5T400-560Zm0-80Zm12 400Z" />
</svg>
);
}
export function TrashIcon(props: IconProps) { export function TrashIcon(props: IconProps) {
return ( return (
<svg <svg
@@ -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;
+52 -24
View File
@@ -1,7 +1,9 @@
"use client"; "use client";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons"; import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import Modal from "@/components/ui/modal";
import Status from "@/components/ui/Status";
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,7 +13,9 @@ import {
TableRow, TableRow,
} 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 Link from "next/link"; import Link from "next/link";
import ChangeStatusModalContent from "./ChangeStatusModalContent";
import { useAdminsPresenter } from "./useAdminsPresenter"; import { useAdminsPresenter } from "./useAdminsPresenter";
const AdminsTable = () => { const AdminsTable = () => {
@@ -22,9 +26,15 @@ const AdminsTable = () => {
onConfirmDelete, onConfirmDelete,
isModalOpen, isModalOpen,
setIsModalOpen, setIsModalOpen,
modalConfig, isChangeStatusModalOpen,
setIsChangeStatusModalOpen,
columns,
setCurrentUser, setCurrentUser,
pathName, pathName,
changeStatus,
currentUser,
setStatus,
status,
} = useAdminsPresenter(); } = useAdminsPresenter();
return ( return (
@@ -35,31 +45,24 @@ const AdminsTable = () => {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-none uppercase [&>th]:text-start"> <TableRow className="border-none uppercase [&>th]:text-start">
<TableHead colSpan={100} className="text-start font-extrabold"> {columns.map((column) => (
Full name <TableHead key={column} colSpan={100}>
</TableHead> {column}
<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> </TableHead>
))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={5} />} {isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allUsers.map((item) => { {allUsers.map((item, index) => {
return ( return (
<TableRow <TableRow
key={item.id} key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7" 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}> <TableCell className="text-start" colSpan={100}>
{item.full_name} {item.full_name}
</TableCell> </TableCell>
@@ -72,6 +75,9 @@ const AdminsTable = () => {
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{item.username} {item.username}
</TableCell> </TableCell>
<TableCell className="text-start" colSpan={100}>
<Status status={item.status ?? ""}>{item.status}</Status>
</TableCell>
<TableCell className="text-start xl:pr-7.5"> <TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.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> <span className="sr-only">Delete Admin </span>
<TrashIcon /> <TrashIcon />
</button> </button>
<button
className="hover:text-yellow-dark"
onClick={() => {
setCurrentUser(item);
setIsChangeStatusModalOpen(true);
}}
>
<span className="sr-only">Change Admin Status</span>
<UserSettingIcon />
</button>
<button <button
className="hover:text-primary" className="hover:text-primary"
onClick={() => router.push(`/admins/edit/${item.id}`)} onClick={() => router.push(`/admins/edit/${item.id}`)}
@@ -99,15 +115,27 @@ const AdminsTable = () => {
})} })}
</TableBody> </TableBody>
</Table> </Table>
<Modal
isOpen={isChangeStatusModalOpen}
onClose={() => setIsChangeStatusModalOpen(false)}
onConfirm={() => {
changeStatus({ id: currentUser?.id!, credentials: { status } });
}}
>
<ChangeStatusModalContent
status={currentUser?.status as AdminStatus}
setStatus={setStatus}
/>
</Modal>
{isModalOpen && ( {isModalOpen && (
<ConfirmationModal <ConfirmationModal
isOpen={isModalOpen} isOpen={isModalOpen}
title={modalConfig.title || "Confirm"} title={"Confirm"}
message={modalConfig.message || "Are you sure?"} message={"Are you sure?"}
variant={modalConfig.variant || "warning"} variant={"warning"}
size={modalConfig.size || "md"} size={"md"}
confirmText={modalConfig.confirmText ?? "Confirm"} confirmText={"Confirm"}
cancelText={modalConfig.cancelText ?? "Cancel"} cancelText={"Cancel"}
onConfirm={() => { onConfirm={() => {
onConfirmDelete(); onConfirmDelete();
}} }}
@@ -1,84 +1,83 @@
"use client"; "use client";
import { useAdminListInfiniteQuery, useDeleteAdmin } from "@/hooks/queries"; import { apiDefaultParams } from "@/constants/Api_Params";
import useDebounce from "@/hooks/useDebounce"; import { AdminStatus } from "@/constants/enums";
import { useState, useEffect, useRef } from "react"; import {
import { z } from "zod"; useAdminListInfiniteQuery,
import { useInView } from "react-intersection-observer"; useChangeAdminStatus,
import { IUser } from "@/lib/api"; useDeleteAdmin,
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal"; } from "@/hooks/queries";
import { IUser, TChangeAdminStatusCredentials } from "@/lib/api";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
export const useAdminsPresenter = () => { export const useAdminsPresenter = () => {
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = 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>(
currentUser?.status ?? AdminStatus.ACTIVE,
);
const router = useRouter(); 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 { const {
data, register,
error, formState: { errors },
isPending, handleSubmit,
fetchNextPage, } = useForm<TChangeAdminStatusCredentials>({
hasNextPage, mode: "onChange",
isFetchingNextPage, defaultValues: {
} = useAdminListInfiniteQuery(params); status: currentUser?.status,
},
});
const [params, setParams] = useState<Record<string, any>>({
...apiDefaultParams,
});
const pathName = usePathname();
const { data, isPending } = useAdminListInfiniteQuery(params);
const { mutate: deleteAdmin } = useDeleteAdmin(() => { const { mutate: deleteAdmin } = useDeleteAdmin(() => {
setIsModalOpen(false); 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 = () => { const onConfirmDelete = () => {
if (currentUser) { if (currentUser) {
deleteAdmin(currentUser.id); deleteAdmin(currentUser.id);
} }
}; };
const { mutate: changeStatus } = useChangeAdminStatus({
id: currentUser?.id ?? "",
successCallback: () => {
setIsChangeStatusModalOpen(false);
},
});
const allUsers = data?.pages.flatMap((page) => page.data.users) ?? []; const allUsers = data?.pages.flatMap((page) => page.data.users) ?? [];
const columns = [
"row",
"full name",
"email",
"role",
"username",
"status",
"actions",
];
return { return {
allUsers, allUsers,
currentAdmin: currentUser, columns,
error,
handleFilterChange,
isFetchingNextPage,
fetchNextPage,
isPending, isPending,
onConfirmDelete, onConfirmDelete,
isModalOpen, isModalOpen,
setIsModalOpen, setIsModalOpen,
modalConfig,
setModalConfig,
search,
hasNextPage,
data,
router, router,
setCurrentUser, setCurrentUser,
pathName, pathName,
status,
setStatus,
changeStatus,
isChangeStatusModalOpen,
currentUser,
setIsChangeStatusModalOpen,
register,
errors,
handleSubmit,
}; };
}; };
@@ -58,6 +58,9 @@ const CompaniesTable = ({}: IProps) => {
key={company.id} key={company.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
> >
<TableHead className="text-start" colSpan={100}>
{index + 1}
</TableHead>
<TableHead className="text-start" colSpan={100}> <TableHead className="text-start" colSpan={100}>
{company.name} {company.name}
</TableHead> </TableHead>
@@ -6,8 +6,7 @@ import {
} from "@/hooks/queries"; } from "@/hooks/queries";
import useDebounce from "@/hooks/useDebounce"; import useDebounce from "@/hooks/useDebounce";
import { TCompany } from "@/lib/api"; import { TCompany } from "@/lib/api";
import { usePathname } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useRouter } from "next/navigation";
import { ChangeEvent, useEffect, useRef, useState } from "react"; import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { z } from "zod"; import { z } from "zod";
@@ -26,6 +25,7 @@ export const useCompanyListPresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const columns = [ const columns = [
"row",
"name", "name",
"email", "email",
"phone", "phone",
+14 -11
View File
@@ -1,5 +1,5 @@
"use client"; "use client";
import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons"; import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { import {
Table, Table,
@@ -12,6 +12,7 @@ import {
import Link from "next/link"; import Link from "next/link";
import useCustomerListPresenter from "./useCustomerListPresenter"; import useCustomerListPresenter from "./useCustomerListPresenter";
import { Building } from "@/components/Layouts/sidebar/icons";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
import TableSkeleton from "@/components/ui/TableSkeleton"; import TableSkeleton from "@/components/ui/TableSkeleton";
@@ -26,7 +27,6 @@ const CustomersTable = () => {
router, router,
columns, columns,
allCustomers, allCustomers,
modalConfig,
isModalOpen, isModalOpen,
assignSelectedCompanies, assignSelectedCompanies,
selectedCompanies, selectedCompanies,
@@ -59,8 +59,9 @@ const CustomersTable = () => {
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={columns.length} />} {isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allCustomers.map((customer) => ( {allCustomers.map((customer, index) => (
<TableRow key={customer.id}> <TableRow key={customer.id}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{customer.full_name}</TableCell> <TableCell colSpan={100}>{customer.full_name}</TableCell>
<TableCell colSpan={100}>{customer.email}</TableCell> <TableCell colSpan={100}>{customer.email}</TableCell>
<TableCell colSpan={100}> <TableCell colSpan={100}>
@@ -82,7 +83,9 @@ const CustomersTable = () => {
<span>None</span> <span>None</span>
)} )}
</TableCell> </TableCell>
<TableCell colSpan={100}>{customer.role ?? "-"}</TableCell> <TableCell colSpan={100} className="uppercase">
{customer.role ?? "-"}
</TableCell>
<TableCell className="text-start xl:pr-7.5"> <TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5"> <div className="flex items-center justify-start gap-x-3.5">
<button <button
@@ -112,7 +115,7 @@ const CustomersTable = () => {
}} }}
> >
<span className="sr-only">Assign To Company </span> <span className="sr-only">Assign To Company </span>
<UserIcon /> <Building />
</button> </button>
</div> </div>
</TableCell> </TableCell>
@@ -139,12 +142,12 @@ const CustomersTable = () => {
{isModalOpen && ( {isModalOpen && (
<ConfirmationModal <ConfirmationModal
isOpen={isModalOpen} isOpen={isModalOpen}
title={modalConfig.title || "Confirm"} title={"Confirm"}
message={modalConfig.message || "Are you sure?"} message={"Are you sure?"}
variant={modalConfig.variant || "default"} variant={"default"}
size={modalConfig.size || "md"} size={"md"}
confirmText={modalConfig.confirmText ?? "Confirm"} confirmText={"Confirm"}
cancelText={modalConfig.cancelText ?? "Cancel"} cancelText={"Cancel"}
onConfirm={() => { onConfirm={() => {
onConfirmDelete(); onConfirmDelete();
}} }}
@@ -1,53 +1,30 @@
"use client"; "use client";
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
import { import {
useAssignCompany, useAssignCompany,
useCustomersInfiniteQuery, useCustomersInfiniteQuery,
useDeleteCustomerQuery, useDeleteCustomerQuery,
} from "@/hooks/queries"; } from "@/hooks/queries";
import useDebounce from "@/hooks/useDebounce";
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api"; import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { ChangeEvent, useEffect, useRef, useState } from "react"; import { useState } from "react";
import { useInView } from "react-intersection-observer";
import { z } from "zod";
const useCustomerListPresenter = () => { const useCustomerListPresenter = () => {
const deleteModal = useRef<HTMLDialogElement | null>(null);
const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] = const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] =
useState(false); useState(false);
const { inView } = useInView({ threshold: 0.5 });
const [selectedCompanies, setSelectedCompanies] = const [selectedCompanies, setSelectedCompanies] =
useState<TAssignCustomerToCompanyCredentials | null>(null); useState<TAssignCustomerToCompanyCredentials | null>(null);
const [currentCustomer, setCurrentCustomer] = useState<TCustomer | null>( const [currentCustomer, setCurrentCustomer] = useState<TCustomer | null>(
null, null,
); );
const [modalConfig, setModalConfig] = useState<
Partial<ConfirmationModalProps>
>({});
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const pathName = usePathname(); const pathName = usePathname();
const [search, setSearch] = useState("");
const [params, setParams] = useState<Record<string, any>>({ const [params, setParams] = useState<Record<string, any>>({
sort: "created_at", sort: "created_at",
}); });
const router = useRouter(); const router = useRouter();
const debouncedSearch = useDebounce(search, 1000); const { data, isPending } = useCustomersInfiniteQuery(params);
const {
data,
error,
isPending,
isError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useCustomersInfiniteQuery(params);
useEffect(() => {
if (inView && hasNextPage && data && data.pages.length < 5) {
fetchNextPage();
}
}, [inView, hasNextPage, data, fetchNextPage]);
const { mutate: deleteCustomer } = useDeleteCustomerQuery(() => const { mutate: deleteCustomer } = useDeleteCustomerQuery(() =>
setIsModalOpen(false), setIsModalOpen(false),
); );
@@ -57,18 +34,8 @@ const useCustomerListPresenter = () => {
setIsAssignCompanyModalOpen(false); setIsAssignCompanyModalOpen(false);
}, },
); );
useEffect(() => {
const result = z.string().safeParse(debouncedSearch);
if (result.success) {
setParams((prevParams) => ({
...prevParams,
}));
}
}, [debouncedSearch]);
const handleFilterChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const columns = [ const columns = [
"row",
"full name", "full name",
"email", "email",
"created at", "created at",
@@ -85,20 +52,10 @@ const useCustomerListPresenter = () => {
}; };
const customers = data?.pages.flatMap((page) => page.data.customers) ?? []; const customers = data?.pages.flatMap((page) => page.data.customers) ?? [];
return { return {
hasNextPage,
fetchNextPage,
allCustomers: customers, allCustomers: customers,
currentCustomer, currentCustomer,
deleteCustomer,
deleteModal,
error,
handleFilterChange,
isError,
isFetchingNextPage,
columns, columns,
data,
isPending, isPending,
search,
setCurrentCustomer, setCurrentCustomer,
selectedCompanies, selectedCompanies,
setSelectedCompanies, setSelectedCompanies,
@@ -108,8 +65,6 @@ const useCustomerListPresenter = () => {
assignSelectedCompanies, assignSelectedCompanies,
pathName, pathName,
router, router,
modalConfig,
setModalConfig,
isModalOpen, isModalOpen,
setIsModalOpen, setIsModalOpen,
}; };
+6 -20
View File
@@ -17,7 +17,7 @@ import TableSkeleton from "@/components/ui/TableSkeleton";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
const TendersTable = () => { const TendersTable = () => {
const { allTenders, isPending, router, pathName, setParams } = const { allTenders, isPending, router, pathName, setParams, columns } =
useTenderListPresenter(); useTenderListPresenter();
return ( return (
@@ -25,29 +25,15 @@ const TendersTable = () => {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-none uppercase [&>th]:text-start"> <TableRow className="border-none uppercase [&>th]:text-start">
<TableHead colSpan={100} className="text-start font-extrabold"> {columns.map((column) => (
Row <TableHead key={column} colSpan={100}>
</TableHead> {column}
<TableHead colSpan={100} className="text-start font-extrabold">
Title
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Country Code
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Submission Url
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Status
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Actions
</TableHead> </TableHead>
))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={6} />} {isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allTenders?.data.tenders?.map((item, index) => {allTenders?.data.tenders?.map((item, index) =>
!item ? ( !item ? (
@@ -34,9 +34,17 @@ const useTenderListPresenter = () => {
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); setSearch(e.target.value);
}; };
const columns = [
"row",
"title",
"country code",
"submission url",
"status",
"actions",
];
return { return {
currentTender, currentTender,
columns,
setCurrentTender, setCurrentTender,
search, search,
handleFilterChange, handleFilterChange,
+1 -1
View File
@@ -26,7 +26,7 @@ const Status = ({ status, children }: IProps) => {
const blues = ["awarded", "processing", "in-review", "scheduled"]; const blues = ["awarded", "processing", "in-review", "scheduled"];
const yellows = ["pending", "warning", "on-hold", "delayed"]; const yellows = ["pending", "warning", "on-hold", "delayed"];
const oranges = ["expired", "expiring", "limited"]; const oranges = ["expired", "expiring", "limited"];
const grays = ["draft", "disabled", "archived", "paused"]; const grays = ["draft", "disabled", "archived", "paused", "suspended"];
return ( return (
<span <span
+8 -3
View File
@@ -1,5 +1,6 @@
import { CrossIcon } from "@/assets/icons"; import { CrossIcon } from "@/assets/icons";
import { useState, useEffect, ReactNode } from "react"; import { useIsMutating } from "@tanstack/react-query";
import { ReactNode, useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
interface ModalProps { interface ModalProps {
@@ -16,7 +17,7 @@ const Modal: React.FC<ModalProps> = ({
onConfirm, onConfirm,
}) => { }) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const isMutating = useIsMutating();
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
@@ -57,7 +58,11 @@ const Modal: React.FC<ModalProps> = ({
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90" className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
onClick={onConfirm} onClick={onConfirm}
> >
Confirm {isMutating ? (
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
) : (
"Confirm"
)}
</button> </button>
<button <button
type="button" type="button"
+5
View File
@@ -5,3 +5,8 @@ export enum TenderStatus {
AWARDED = "awarded", AWARDED = "awarded",
DRAFT = "draft", DRAFT = "draft",
} }
export enum AdminStatus {
ACTIVE = "active",
INACTIVE = "inactive",
SUSPENDED = "suspended",
}
+24
View File
@@ -121,3 +121,27 @@ export const useAdminListInfiniteQuery = (params?: Record<string, any>) => {
}, },
}); });
}; };
export const useChangeAdminStatus = ({
id,
successCallback,
}: {
id: string;
successCallback: () => void;
}) => {
const queryClient = useQueryClient();
const mutationKey = useMemo(
() => [API_ENDPOINTS.USER.CHANGE_ADMIN_STATUS(id)],
[id],
);
return useMutation({
mutationKey,
mutationFn: userService.changeAdminStatus,
onSuccess: ({ message }: { message: string }) => {
successCallback();
toast.success(message);
queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.USER.ADMINS],
});
},
});
};
+2 -1
View File
@@ -9,6 +9,7 @@ export const API_ENDPOINTS = {
REFRESH_TOKEN: "profile/refresh-token", REFRESH_TOKEN: "profile/refresh-token",
ADMINS: "users", ADMINS: "users",
ADMIN_DETAILS: (id: string) => `users/${id}`, ADMIN_DETAILS: (id: string) => `users/${id}`,
CHANGE_ADMIN_STATUS: (id: string) => `users/${id}/status`,
}, },
PROFILE: { PROFILE: {
READ: "profile", READ: "profile",
@@ -25,7 +26,7 @@ export const API_ENDPOINTS = {
UPDATE: (id: string) => `company-categories/${id}`, UPDATE: (id: string) => `company-categories/${id}`,
DELETE: (id: string) => `company-categories/${id}`, DELETE: (id: string) => `company-categories/${id}`,
DETAILS: (id: string) => `company-categories/${id}`, DETAILS: (id: string) => `company-categories/${id}`,
TOGGLE_PUBLISHED:(id:string) => `company-categories/${id}/publish`, TOGGLE_PUBLISHED: (id: string) => `company-categories/${id}/publish`,
}, },
}, },
TENDERS: { TENDERS: {
+17
View File
@@ -9,6 +9,7 @@ import {
ILogoutResponse, ILogoutResponse,
LoginResponseSchema, LoginResponseSchema,
LogoutResponseSchema, LogoutResponseSchema,
TChangeAdminStatusCredentials,
} from "../types"; } from "../types";
export const userService = { export const userService = {
@@ -87,4 +88,20 @@ export const userService = {
throw error; throw error;
} }
}, },
changeAdminStatus: async ({
id,
credentials,
}: {
id: string;
credentials: TChangeAdminStatusCredentials;
}) => {
try {
return (
await api.put(API_ENDPOINTS.USER.CHANGE_ADMIN_STATUS(id), credentials)
).data;
} catch (error) {
console.error("ERROR Caught in user service => change status", error);
throw error;
}
},
}; };
+6 -2
View File
@@ -58,13 +58,17 @@ export const LoginResponse = z.object({
export const LogoutResponse = z.object({ export const LogoutResponse = z.object({
message: z.string(), message: z.string(),
}); });
export const changeAdminStatusCredentialsSchema = z.object({
status: z.string(),
});
export type IUser = z.infer<typeof UserSchema>; export type IUser = z.infer<typeof UserSchema>;
export type ILogoutResponse = z.infer<typeof LogoutResponse>; export type ILogoutResponse = z.infer<typeof LogoutResponse>;
export type ILoginCredentials = z.infer<typeof LoginCredentials>; export type ILoginCredentials = z.infer<typeof LoginCredentials>;
export type ILoginResponse = z.infer<typeof LoginResponse>; export type ILoginResponse = z.infer<typeof LoginResponse>;
export type IAdminListResponse = z.infer<typeof AdminListResponse>; export type IAdminListResponse = z.infer<typeof AdminListResponse>;
export type TChangeAdminStatusCredentials = z.infer<
typeof changeAdminStatusCredentialsSchema
>;
export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse); export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse);
export const LoginResponseSchema = createApiResponseSchema(LoginResponse); export const LoginResponseSchema = createApiResponseSchema(LoginResponse);
export const AdminListResponseSchema = export const AdminListResponseSchema =
+7 -1
View File
@@ -6,7 +6,6 @@ export const msToDate = (ms: number) => {
return moment.default(ms).format("YYYY/MM/DD"); return moment.default(ms).format("YYYY/MM/DD");
}; };
export const getStatusColor = <T>(status: T) => { export const getStatusColor = <T>(status: T) => {
switch (status) { switch (status) {
case "active": case "active":
@@ -41,3 +40,10 @@ export const formatPhoneNumber = (input?: string) => {
return (hasPlus ? "+" : "") + groups.join(" "); return (hasPlus ? "+" : "") + groups.join(" ");
}; };
export const capitalize = (text: string) => {
return text
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ")
.trim();
};