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) {
|
||||
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
|
||||
{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}>
|
||||
@@ -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
|
||||
{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