Merge pull request 'feat(admins): implement admin status management' (#33) from change-admin-status into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/33 Reviewed-by: Nima Nakhsotin <n.nakhostin@ravanertebat.com>
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) {
|
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;
|
||||||
@@ -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">
|
</TableHead>
|
||||||
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>
|
|
||||||
</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",
|
||||||
|
|||||||
@@ -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}>
|
||||||
@@ -71,7 +72,7 @@ const CustomersTable = () => {
|
|||||||
{customer.type}
|
{customer.type}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
<Status status={customer.status}>{customer.status}</Status>
|
<Status status={customer.status}>{customer.status}</Status>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
{customer.companies?.length ? (
|
{customer.companies?.length ? (
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
</TableHead>
|
||||||
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>
|
|
||||||
</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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,3 +5,8 @@ export enum TenderStatus {
|
|||||||
AWARDED = "awarded",
|
AWARDED = "awarded",
|
||||||
DRAFT = "draft",
|
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",
|
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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user