diff --git a/src/assets/icons.tsx b/src/assets/icons.tsx index eed4ddc..7c3406b 100644 --- a/src/assets/icons.tsx +++ b/src/assets/icons.tsx @@ -215,6 +215,20 @@ export function GlobeIcon(props: IconProps) { ); } +export function UserSettingIcon(props: IconProps) { + return ( + + + + ); +} export function TrashIcon(props: IconProps) { return ( >; +} +const ChangeStatusModalContent: FC = ({ status, setStatus }) => { + const { register } = useForm({ + mode: "onChange", + defaultValues: { status }, + }); + const adminStatusEntries = Object.entries(AdminStatus).map( + ([key, value]) => ({ + label: capitalize(key), + value, + }), + ); + + return ( + + + + ); +}; + +export default ChangeStatusModalContent; diff --git a/src/components/Tables/admins/index.tsx b/src/components/Tables/admins/index.tsx index 1442972..92720a5 100644 --- a/src/components/Tables/admins/index.tsx +++ b/src/components/Tables/admins/index.tsx @@ -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 = () => { - - Full name - - - Email - - - Role - - - Username - - - Actions - + {columns.map((column) => ( + + {column} + + ))} - {isPending && } + {isPending && } - {allUsers.map((item) => { + {allUsers.map((item, index) => { return ( + + {index + 1} + {item.full_name} @@ -72,6 +75,9 @@ const AdminsTable = () => { {item.username} + + {item.status} + @@ -85,6 +91,16 @@ const AdminsTable = () => { Delete Admin + { + setCurrentUser(item); + setIsChangeStatusModalOpen(true); + }} + > + Change Admin Status + + router.push(`/admins/edit/${item.id}`)} @@ -99,15 +115,27 @@ const AdminsTable = () => { })} + setIsChangeStatusModalOpen(false)} + onConfirm={() => { + changeStatus({ id: currentUser?.id!, credentials: { status } }); + }} + > + + {isModalOpen && ( { onConfirmDelete(); }} diff --git a/src/components/Tables/admins/useAdminsPresenter.ts b/src/components/Tables/admins/useAdminsPresenter.ts index 0f52de6..8f63241 100644 --- a/src/components/Tables/admins/useAdminsPresenter.ts +++ b/src/components/Tables/admins/useAdminsPresenter.ts @@ -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(null); const [isModalOpen, setIsModalOpen] = useState(false); + const [status, setStatus] = useState( + currentUser?.status ?? AdminStatus.ACTIVE, + ); const router = useRouter(); - const [modalConfig, setModalConfig] = useState< - Partial - >({}); - - const [search, setSearch] = useState(""); - - const [params, setParams] = useState>({ - 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({ + mode: "onChange", + defaultValues: { + status: currentUser?.status, + }, + }); + const [params, setParams] = useState>({ + ...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) => { - 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, }; }; diff --git a/src/components/Tables/companies/index.tsx b/src/components/Tables/companies/index.tsx index f94cda6..f3c11de 100644 --- a/src/components/Tables/companies/index.tsx +++ b/src/components/Tables/companies/index.tsx @@ -58,6 +58,9 @@ const CompaniesTable = ({}: IProps) => { key={company.id} className="odd:bg-gray-2 dark:odd:bg-gray-7" > + + {index + 1} + {company.name} diff --git a/src/components/Tables/companies/useCompanyListPresenter.ts b/src/components/Tables/companies/useCompanyListPresenter.ts index 3f82769..3be8313 100644 --- a/src/components/Tables/companies/useCompanyListPresenter.ts +++ b/src/components/Tables/companies/useCompanyListPresenter.ts @@ -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", diff --git a/src/components/Tables/customers/index.tsx b/src/components/Tables/customers/index.tsx index a02da66..79c303f 100644 --- a/src/components/Tables/customers/index.tsx +++ b/src/components/Tables/customers/index.tsx @@ -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 = () => { {isPending && } - {allCustomers.map((customer) => ( + {allCustomers.map((customer, index) => ( + {index + 1} {customer.full_name} {customer.email} @@ -71,7 +72,7 @@ const CustomersTable = () => { {customer.type} - {customer.status} + {customer.status} {customer.companies?.length ? ( @@ -82,7 +83,9 @@ const CustomersTable = () => { None )} - {customer.role ?? "-"} + + {customer.role ?? "-"} + { }} > Assign To Company - + @@ -139,12 +142,12 @@ const CustomersTable = () => { {isModalOpen && ( { onConfirmDelete(); }} diff --git a/src/components/Tables/customers/useCustomerListPresenter.ts b/src/components/Tables/customers/useCustomerListPresenter.ts index 4a5facd..9cc641c 100644 --- a/src/components/Tables/customers/useCustomerListPresenter.ts +++ b/src/components/Tables/customers/useCustomerListPresenter.ts @@ -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(null); const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] = useState(false); - const { inView } = useInView({ threshold: 0.5 }); const [selectedCompanies, setSelectedCompanies] = useState(null); const [currentCustomer, setCurrentCustomer] = useState( null, ); - const [modalConfig, setModalConfig] = useState< - Partial - >({}); + const [isModalOpen, setIsModalOpen] = useState(false); const pathName = usePathname(); - const [search, setSearch] = useState(""); const [params, setParams] = useState>({ 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) => { - 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, }; diff --git a/src/components/Tables/tenders/index.tsx b/src/components/Tables/tenders/index.tsx index aa43cff..ebd76b4 100644 --- a/src/components/Tables/tenders/index.tsx +++ b/src/components/Tables/tenders/index.tsx @@ -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 = () => { - - Row - - - Title - - - Country Code - - - Submission Url - - - Status - - - - Actions - + {columns.map((column) => ( + + {column} + + ))} - {isPending && } + {isPending && } {allTenders?.data.tenders?.map((item, index) => !item ? ( diff --git a/src/components/Tables/tenders/useTenderListPresenter.ts b/src/components/Tables/tenders/useTenderListPresenter.ts index 9c43feb..41f3b47 100644 --- a/src/components/Tables/tenders/useTenderListPresenter.ts +++ b/src/components/Tables/tenders/useTenderListPresenter.ts @@ -34,9 +34,17 @@ const useTenderListPresenter = () => { const handleFilterChange = (e: React.ChangeEvent) => { setSearch(e.target.value); }; - + const columns = [ + "row", + "title", + "country code", + "submission url", + "status", + "actions", + ]; return { currentTender, + columns, setCurrentTender, search, handleFilterChange, diff --git a/src/components/ui/Status.tsx b/src/components/ui/Status.tsx index 24e00d1..4684e37 100644 --- a/src/components/ui/Status.tsx +++ b/src/components/ui/Status.tsx @@ -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 ( = ({ onConfirm, }) => { const [mounted, setMounted] = useState(false); - + const isMutating = useIsMutating(); useEffect(() => { setMounted(true); }, []); @@ -57,7 +58,11 @@ const Modal: React.FC = ({ 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 ? ( + + ) : ( + "Confirm" + )} ) => { }, }); }; +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], + }); + }, + }); +}; diff --git a/src/lib/api/endpoints.ts b/src/lib/api/endpoints.ts index d747059..d168ab6 100644 --- a/src/lib/api/endpoints.ts +++ b/src/lib/api/endpoints.ts @@ -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: { diff --git a/src/lib/api/services/user-services.ts b/src/lib/api/services/user-services.ts index 1d58935..e59cf47 100644 --- a/src/lib/api/services/user-services.ts +++ b/src/lib/api/services/user-services.ts @@ -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; + } + }, }; diff --git a/src/lib/api/types/User.ts b/src/lib/api/types/User.ts index 8b2de37..3e51c01 100644 --- a/src/lib/api/types/User.ts +++ b/src/lib/api/types/User.ts @@ -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; export type ILogoutResponse = z.infer; export type ILoginCredentials = z.infer; export type ILoginResponse = z.infer; export type IAdminListResponse = z.infer; - +export type TChangeAdminStatusCredentials = z.infer< + typeof changeAdminStatusCredentialsSchema +>; export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse); export const LoginResponseSchema = createApiResponseSchema(LoginResponse); export const AdminListResponseSchema = diff --git a/src/utils/shared.ts b/src/utils/shared.ts index a248ed0..c5d6664 100644 --- a/src/utils/shared.ts +++ b/src/utils/shared.ts @@ -6,7 +6,6 @@ export const msToDate = (ms: number) => { return moment.default(ms).format("YYYY/MM/DD"); }; - export const getStatusColor = (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(); +};