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:
@@ -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) {
|
||||
return (
|
||||
<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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,6 +58,9 @@ const CompaniesTable = ({}: IProps) => {
|
||||
key={company.id}
|
||||
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}>
|
||||
{company.name}
|
||||
</TableHead>
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
} from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { TCompany } from "@/lib/api";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { z } from "zod";
|
||||
@@ -26,6 +25,7 @@ export const useCompanyListPresenter = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const columns = [
|
||||
"row",
|
||||
"name",
|
||||
"email",
|
||||
"phone",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
Table,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import useCustomerListPresenter from "./useCustomerListPresenter";
|
||||
|
||||
import { Building } from "@/components/Layouts/sidebar/icons";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import Status from "@/components/ui/Status";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
@@ -26,7 +27,6 @@ const CustomersTable = () => {
|
||||
router,
|
||||
columns,
|
||||
allCustomers,
|
||||
modalConfig,
|
||||
isModalOpen,
|
||||
assignSelectedCompanies,
|
||||
selectedCompanies,
|
||||
@@ -59,8 +59,9 @@ const CustomersTable = () => {
|
||||
</TableHeader>
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{allCustomers.map((customer) => (
|
||||
{allCustomers.map((customer, index) => (
|
||||
<TableRow key={customer.id}>
|
||||
<TableCell colSpan={100}>{index + 1}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.full_name}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.email}</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
@@ -71,7 +72,7 @@ const CustomersTable = () => {
|
||||
{customer.type}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<Status status={customer.status}>{customer.status}</Status>
|
||||
<Status status={customer.status}>{customer.status}</Status>
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{customer.companies?.length ? (
|
||||
@@ -82,7 +83,9 @@ const CustomersTable = () => {
|
||||
<span>None</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>{customer.role ?? "-"}</TableCell>
|
||||
<TableCell colSpan={100} className="uppercase">
|
||||
{customer.role ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
@@ -112,7 +115,7 @@ const CustomersTable = () => {
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Assign To Company </span>
|
||||
<UserIcon />
|
||||
<Building />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -139,12 +142,12 @@ const CustomersTable = () => {
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
title={modalConfig.title || "Confirm"}
|
||||
message={modalConfig.message || "Are you sure?"}
|
||||
variant={modalConfig.variant || "default"}
|
||||
size={modalConfig.size || "md"}
|
||||
confirmText={modalConfig.confirmText ?? "Confirm"}
|
||||
cancelText={modalConfig.cancelText ?? "Cancel"}
|
||||
title={"Confirm"}
|
||||
message={"Are you sure?"}
|
||||
variant={"default"}
|
||||
size={"md"}
|
||||
confirmText={"Confirm"}
|
||||
cancelText={"Cancel"}
|
||||
onConfirm={() => {
|
||||
onConfirmDelete();
|
||||
}}
|
||||
|
||||
@@ -1,53 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
useAssignCompany,
|
||||
useCustomersInfiniteQuery,
|
||||
useDeleteCustomerQuery,
|
||||
} from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { z } from "zod";
|
||||
import { useState } from "react";
|
||||
|
||||
const useCustomerListPresenter = () => {
|
||||
const deleteModal = useRef<HTMLDialogElement | null>(null);
|
||||
const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] =
|
||||
useState(false);
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
const [selectedCompanies, setSelectedCompanies] =
|
||||
useState<TAssignCustomerToCompanyCredentials | null>(null);
|
||||
const [currentCustomer, setCurrentCustomer] = useState<TCustomer | null>(
|
||||
null,
|
||||
);
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
Partial<ConfirmationModalProps>
|
||||
>({});
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const pathName = usePathname();
|
||||
const [search, setSearch] = useState("");
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const router = useRouter();
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
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 { data, isPending } = useCustomersInfiniteQuery(params);
|
||||
const { mutate: deleteCustomer } = useDeleteCustomerQuery(() =>
|
||||
setIsModalOpen(false),
|
||||
);
|
||||
@@ -57,18 +34,8 @@ const useCustomerListPresenter = () => {
|
||||
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 = [
|
||||
"row",
|
||||
"full name",
|
||||
"email",
|
||||
"created at",
|
||||
@@ -85,20 +52,10 @@ const useCustomerListPresenter = () => {
|
||||
};
|
||||
const customers = data?.pages.flatMap((page) => page.data.customers) ?? [];
|
||||
return {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
allCustomers: customers,
|
||||
currentCustomer,
|
||||
deleteCustomer,
|
||||
deleteModal,
|
||||
error,
|
||||
handleFilterChange,
|
||||
isError,
|
||||
isFetchingNextPage,
|
||||
columns,
|
||||
data,
|
||||
isPending,
|
||||
search,
|
||||
setCurrentCustomer,
|
||||
selectedCompanies,
|
||||
setSelectedCompanies,
|
||||
@@ -108,8 +65,6 @@ const useCustomerListPresenter = () => {
|
||||
assignSelectedCompanies,
|
||||
pathName,
|
||||
router,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
const TendersTable = () => {
|
||||
const { allTenders, isPending, router, pathName, setParams } =
|
||||
const { allTenders, isPending, router, pathName, setParams, columns } =
|
||||
useTenderListPresenter();
|
||||
|
||||
return (
|
||||
@@ -25,29 +25,15 @@ const TendersTable = () => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Row
|
||||
</TableHead>
|
||||
<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>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column} colSpan={100}>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
{isPending && <TableSkeleton column={6} />}
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{allTenders?.data.tenders?.map((item, index) =>
|
||||
!item ? (
|
||||
|
||||
@@ -34,9 +34,17 @@ const useTenderListPresenter = () => {
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
"row",
|
||||
"title",
|
||||
"country code",
|
||||
"submission url",
|
||||
"status",
|
||||
"actions",
|
||||
];
|
||||
return {
|
||||
currentTender,
|
||||
columns,
|
||||
setCurrentTender,
|
||||
search,
|
||||
handleFilterChange,
|
||||
|
||||
@@ -26,7 +26,7 @@ const Status = ({ status, children }: IProps) => {
|
||||
const blues = ["awarded", "processing", "in-review", "scheduled"];
|
||||
const yellows = ["pending", "warning", "on-hold", "delayed"];
|
||||
const oranges = ["expired", "expiring", "limited"];
|
||||
const grays = ["draft", "disabled", "archived", "paused"];
|
||||
const grays = ["draft", "disabled", "archived", "paused", "suspended"];
|
||||
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
interface ModalProps {
|
||||
@@ -16,7 +17,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
onConfirm,
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const isMutating = useIsMutating();
|
||||
useEffect(() => {
|
||||
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"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Confirm
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
"Confirm"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,3 +5,8 @@ export enum TenderStatus {
|
||||
AWARDED = "awarded",
|
||||
DRAFT = "draft",
|
||||
}
|
||||
export enum AdminStatus {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
SUSPENDED = "suspended",
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export const API_ENDPOINTS = {
|
||||
REFRESH_TOKEN: "profile/refresh-token",
|
||||
ADMINS: "users",
|
||||
ADMIN_DETAILS: (id: string) => `users/${id}`,
|
||||
CHANGE_ADMIN_STATUS: (id: string) => `users/${id}/status`,
|
||||
},
|
||||
PROFILE: {
|
||||
READ: "profile",
|
||||
@@ -25,7 +26,7 @@ export const API_ENDPOINTS = {
|
||||
UPDATE: (id: string) => `company-categories/${id}`,
|
||||
DELETE: (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: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ILogoutResponse,
|
||||
LoginResponseSchema,
|
||||
LogoutResponseSchema,
|
||||
TChangeAdminStatusCredentials,
|
||||
} from "../types";
|
||||
|
||||
export const userService = {
|
||||
@@ -87,4 +88,20 @@ export const userService = {
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,13 +58,17 @@ export const LoginResponse = z.object({
|
||||
export const LogoutResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const changeAdminStatusCredentialsSchema = z.object({
|
||||
status: z.string(),
|
||||
});
|
||||
export type IUser = z.infer<typeof UserSchema>;
|
||||
export type ILogoutResponse = z.infer<typeof LogoutResponse>;
|
||||
export type ILoginCredentials = z.infer<typeof LoginCredentials>;
|
||||
export type ILoginResponse = z.infer<typeof LoginResponse>;
|
||||
export type IAdminListResponse = z.infer<typeof AdminListResponse>;
|
||||
|
||||
export type TChangeAdminStatusCredentials = z.infer<
|
||||
typeof changeAdminStatusCredentialsSchema
|
||||
>;
|
||||
export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse);
|
||||
export const LoginResponseSchema = createApiResponseSchema(LoginResponse);
|
||||
export const AdminListResponseSchema =
|
||||
|
||||
+7
-1
@@ -6,7 +6,6 @@ export const msToDate = (ms: number) => {
|
||||
return moment.default(ms).format("YYYY/MM/DD");
|
||||
};
|
||||
|
||||
|
||||
export const getStatusColor = <T>(status: T) => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
@@ -41,3 +40,10 @@ export const formatPhoneNumber = (input?: string) => {
|
||||
|
||||
return (hasPlus ? "+" : "") + groups.join(" ");
|
||||
};
|
||||
export const capitalize = (text: string) => {
|
||||
return text
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ")
|
||||
.trim();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user