feat(companies): implement companies page with data table
This commit introduces a new "Companies" page to the dashboard, providing a comprehensive view of company data in a sortable and paginated table. Key changes include: - A new page at `/companies` to display the data table. - A new API route `/api/companies` that serves mock company data. - A `CompaniesTable` component built with `react-table` featuring sorting, pagination, and search functionality. - Reusable `TablePagination` and `TableActions` components to support the table. - The `moment` library has been added as a dependency to format dates within the table. - A new `CrossIcon` has been added for UI controls. - The `InputGroup` component is enhanced with an `autoComplete` prop. - A link to the new "Companies" page has been added to the sidebar navigation.
This commit is contained in:
Generated
+10
@@ -18,6 +18,7 @@
|
|||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsvectormap": "^1.6.0",
|
"jsvectormap": "^1.6.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"next": "15.1.6",
|
"next": "15.1.6",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"nextjs-toploader": "^3.7.15",
|
"nextjs-toploader": "^3.7.15",
|
||||||
@@ -4452,6 +4453,15 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsvectormap": "^1.6.0",
|
"jsvectormap": "^1.6.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"next": "15.1.6",
|
"next": "15.1.6",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"nextjs-toploader": "^3.7.15",
|
"nextjs-toploader": "^3.7.15",
|
||||||
|
|||||||
@@ -344,6 +344,20 @@ export function GoogleIcon(props: IconProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export function CrossIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
width="24"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function UserIcon(props: IconProps) {
|
export function UserIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function SigninWithPassword() {
|
|||||||
const { handleSubmit, control, reset, register } =
|
const { handleSubmit, control, reset, register } =
|
||||||
useForm<ILoginCredentials>();
|
useForm<ILoginCredentials>();
|
||||||
const { mutate, isPending } = useLoginQuery();
|
const { mutate, isPending } = useLoginQuery();
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
||||||
mutate(data);
|
mutate(data);
|
||||||
reset();
|
reset();
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { type HTMLInputTypeAttribute, useId } from "react";
|
import {
|
||||||
|
HTMLInputAutoCompleteAttribute,
|
||||||
|
type HTMLInputTypeAttribute,
|
||||||
|
useId,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
type FieldErrors,
|
type FieldErrors,
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
@@ -23,6 +27,7 @@ type InputGroupProps<T extends FieldValues> = {
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
iconPosition?: "left" | "right";
|
iconPosition?: "left" | "right";
|
||||||
height?: "sm" | "default";
|
height?: "sm" | "default";
|
||||||
|
autoComplete?: HTMLInputAutoCompleteAttribute;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
register?: UseFormRegister<T>;
|
register?: UseFormRegister<T>;
|
||||||
errors?: FieldErrors<T>;
|
errors?: FieldErrors<T>;
|
||||||
@@ -41,7 +46,7 @@ const InputGroup = <T extends FieldValues>({
|
|||||||
name,
|
name,
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
|
autoComplete,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
ref,
|
ref,
|
||||||
@@ -75,6 +80,7 @@ const InputGroup = <T extends FieldValues>({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
name={name}
|
name={name}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
autoComplete={autoComplete}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange?.(e);
|
onChange?.(e);
|
||||||
handleChange?.(e);
|
handleChange?.(e);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { ChevronUpIcon } from "@/assets/icons";
|
import { ChevronUpIcon } from "@/assets/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useId, useState } from "react";
|
import { useId, useState, type ChangeEvent } from "react";
|
||||||
import {
|
import {
|
||||||
type FieldErrors,
|
type FieldErrors,
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
@@ -16,6 +16,7 @@ type PropsType<T extends FieldValues> = {
|
|||||||
label: string;
|
label: string;
|
||||||
items: { value: string; label: string }[];
|
items: { value: string; label: string }[];
|
||||||
prefixIcon?: React.ReactNode;
|
prefixIcon?: React.ReactNode;
|
||||||
|
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
register?: UseFormRegister<T>;
|
register?: UseFormRegister<T>;
|
||||||
errors?: FieldErrors<T>;
|
errors?: FieldErrors<T>;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const NAV_DATA: NavData = [
|
|||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "companies",
|
title: "Companies",
|
||||||
url: "/companies",
|
url: "/companies",
|
||||||
icon: Icons.Building,
|
icon: Icons.Building,
|
||||||
items: [],
|
items: [],
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
FC,
|
||||||
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||||
|
import { useCompanyFullList } from "@/hooks/queries";
|
||||||
|
import { ILabelValue } from "@/types/shared";
|
||||||
|
import { Select } from "@/components/FormElements/select";
|
||||||
|
import { Building } from "@/components/Layouts/sidebar/icons";
|
||||||
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
setCompanies: Dispatch<
|
||||||
|
SetStateAction<TAssignCustomerToCompanyCredentials | null>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
const AssignToCompanyModalContent: FC<IProps> = ({ setCompanies }) => {
|
||||||
|
const [options, setOptions] = useState<ILabelValue[]>([]);
|
||||||
|
|
||||||
|
const { data, status, isSuccess } = useCompanyFullList();
|
||||||
|
const { handleSubmit, register } =
|
||||||
|
useForm<TAssignCustomerToCompanyCredentials>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccess) {
|
||||||
|
setOptions(
|
||||||
|
data.data.companies
|
||||||
|
? data.data.companies?.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
const assignCompany = (data: TAssignCustomerToCompanyCredentials) => {
|
||||||
|
setCompanies(data);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<form className="mt-5" onSubmit={handleSubmit(assignCompany)}>
|
||||||
|
<Select
|
||||||
|
{...register("company_ids", {
|
||||||
|
required: { message: FormErrorMessages.required, value: true },
|
||||||
|
onChange: (e) => {
|
||||||
|
setCompanies({
|
||||||
|
company_ids: [e.target.value],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
name="company_ids"
|
||||||
|
label="Companies"
|
||||||
|
items={options}
|
||||||
|
prefixIcon={<Building />}
|
||||||
|
placeholder="Please Select company"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssignToCompanyModalContent;
|
||||||
@@ -10,8 +10,13 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons";
|
||||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||||
|
|
||||||
|
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
const CustomersTable = () => {
|
const CustomersTable = () => {
|
||||||
const {
|
const {
|
||||||
isPending,
|
isPending,
|
||||||
@@ -21,15 +26,21 @@ const CustomersTable = () => {
|
|||||||
allCustomers,
|
allCustomers,
|
||||||
modalConfig,
|
modalConfig,
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
|
assignSelectedCompanies,
|
||||||
|
selectedCompanies,
|
||||||
|
currentCustomer,
|
||||||
|
setSelectedCompanies,
|
||||||
setIsModalOpen,
|
setIsModalOpen,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
setCurrentCustomer,
|
setCurrentCustomer,
|
||||||
|
isAssignCompanyModalOpen,
|
||||||
|
setIsAssignCompanyModalOpen,
|
||||||
} = useCustomerListPresenter();
|
} = useCustomerListPresenter();
|
||||||
if (isPending) return <CustomersSkeleton />;
|
if (isPending) return <CustomersSkeleton />;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||||
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90">
|
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90">
|
||||||
<Link href={`${pathName}/create`}>Create Company</Link>
|
<Link href={`${pathName}/create`}>Create Customer </Link>
|
||||||
</button>
|
</button>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -83,12 +94,38 @@ const CustomersTable = () => {
|
|||||||
<span className="sr-only">Edit Company </span>
|
<span className="sr-only">Edit Company </span>
|
||||||
<PencilSquareIcon />
|
<PencilSquareIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="hover:text-green-dark"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentCustomer(customer);
|
||||||
|
setIsAssignCompanyModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Assign To Company </span>
|
||||||
|
<UserIcon />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<Modal
|
||||||
|
onConfirm={() => {
|
||||||
|
if (!selectedCompanies || !currentCustomer) {
|
||||||
|
toast.error("Invalid credentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assignSelectedCompanies({
|
||||||
|
credentials: selectedCompanies,
|
||||||
|
id: currentCustomer?.id ?? "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isOpen={isAssignCompanyModalOpen}
|
||||||
|
onClose={() => setIsAssignCompanyModalOpen(false)}
|
||||||
|
>
|
||||||
|
<AssignToCompanyModalContent setCompanies={setSelectedCompanies} />
|
||||||
|
</Modal>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const useCustomerListPresenter = () => {
|
const useCustomerListPresenter = () => {
|
||||||
const deleteModal = useRef<HTMLDialogElement | null>(null);
|
const deleteModal = useRef<HTMLDialogElement | null>(null);
|
||||||
const assignToCompanyModal = useRef<HTMLDialogElement | null>(null);
|
const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] =
|
||||||
|
useState(false);
|
||||||
const { inView } = useInView({ threshold: 0.5 });
|
const { inView } = useInView({ threshold: 0.5 });
|
||||||
const [selectedCompanies, setSelectedCompanies] =
|
const [selectedCompanies, setSelectedCompanies] =
|
||||||
useState<TAssignCustomerToCompanyCredentials | null>(null);
|
useState<TAssignCustomerToCompanyCredentials | null>(null);
|
||||||
@@ -53,7 +54,7 @@ const useCustomerListPresenter = () => {
|
|||||||
const { mutate: assignSelectedCompanies } = useAssignCompany(
|
const { mutate: assignSelectedCompanies } = useAssignCompany(
|
||||||
currentCustomer?.id ?? "",
|
currentCustomer?.id ?? "",
|
||||||
() => {
|
() => {
|
||||||
assignToCompanyModal.current?.close();
|
setIsAssignCompanyModalOpen(false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -101,7 +102,8 @@ const useCustomerListPresenter = () => {
|
|||||||
selectedCompanies,
|
selectedCompanies,
|
||||||
setSelectedCompanies,
|
setSelectedCompanies,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
assignToCompanyModal,
|
setIsAssignCompanyModalOpen,
|
||||||
|
isAssignCompanyModalOpen,
|
||||||
assignSelectedCompanies,
|
assignSelectedCompanies,
|
||||||
pathName,
|
pathName,
|
||||||
router,
|
router,
|
||||||
|
|||||||
@@ -12,9 +12,19 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
import { TendersSkeleton } from "./Skeleton";
|
import { TendersSkeleton } from "./Skeleton";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getStatusColor } from "@/utils/shared";
|
||||||
const TendersTable = () => {
|
const TendersTable = () => {
|
||||||
const { allTenders, isPending } = useTenderListPresenter();
|
const {
|
||||||
if (isPending) return <TendersSkeleton />;
|
allTenders,
|
||||||
|
isPending,
|
||||||
|
ref,
|
||||||
|
isFetchingNextPage,
|
||||||
|
data,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
} = useTenderListPresenter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -38,6 +48,7 @@ const TendersTable = () => {
|
|||||||
<h6 key={index}>No tenders were found</h6>
|
<h6 key={index}>No tenders were found</h6>
|
||||||
) : (
|
) : (
|
||||||
<TableRow
|
<TableRow
|
||||||
|
ref={index === allTenders.length - 1 ? ref : null}
|
||||||
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"
|
||||||
>
|
>
|
||||||
@@ -47,14 +58,31 @@ const TendersTable = () => {
|
|||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.country_code}
|
{item.country_code}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||||
{item.status}
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl p-2",
|
||||||
|
getStatusColor(item.status),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.status}
|
||||||
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{isFetchingNextPage && <TendersSkeleton />}
|
||||||
|
{(data?.pages.length ?? 0) >= 5 && hasNextPage && (
|
||||||
|
<button
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
className="mt-4 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? "Loading..." : "Load More"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,20 +28,20 @@ const useTenderListPresenter = () => {
|
|||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
} = useTendersInfiniteQuery(params);
|
} = useTendersInfiniteQuery(params);
|
||||||
|
|
||||||
const { inView } = useInView({ threshold: 0.5 });
|
const { ref, inView } = useInView({ threshold: 0.5 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
if (inView && hasNextPage && (data?.pages.length ?? 0) < 5) {
|
||||||
fetchNextPage();
|
fetchNextPage();
|
||||||
}
|
}
|
||||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
}, [inView, hasNextPage, fetchNextPage, data?.pages.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const result = z.string().safeParse(debouncedSearch);
|
const result = z.string().safeParse(debouncedSearch);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setParams((prevParams) => ({
|
setParams((prevParams) => ({
|
||||||
...prevParams,
|
...prevParams,
|
||||||
q: result.data,
|
search: debouncedSearch,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [debouncedSearch]);
|
}, [debouncedSearch]);
|
||||||
@@ -57,6 +57,7 @@ const useTenderListPresenter = () => {
|
|||||||
|
|
||||||
const allTenders = data?.pages.flatMap((page) => page.data.tenders) ?? [];
|
const allTenders = data?.pages.flatMap((page) => page.data.tenders) ?? [];
|
||||||
return {
|
return {
|
||||||
|
ref,
|
||||||
currentTender,
|
currentTender,
|
||||||
setCurrentTender,
|
setCurrentTender,
|
||||||
search,
|
search,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
|||||||
<InputGroup
|
<InputGroup
|
||||||
label="Username"
|
label="Username"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
|
autoComplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
{...register("username", {
|
{...register("username", {
|
||||||
required: {
|
required: {
|
||||||
@@ -90,6 +91,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
|||||||
<div>
|
<div>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
label="Email"
|
label="Email"
|
||||||
|
autoComplete="off"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
type="email"
|
type="email"
|
||||||
{...register("email", {
|
{...register("email", {
|
||||||
@@ -115,6 +117,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
|||||||
<InputGroup
|
<InputGroup
|
||||||
label="Password"
|
label="Password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
autoComplete="off"
|
||||||
type="password"
|
type="password"
|
||||||
{...register("password", {
|
{...register("password", {
|
||||||
required: {
|
required: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import InputGroup from "@/components/FormElements/InputGroup";
|
|||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
import { Select } from "@/components/FormElements/select";
|
import { Select } from "@/components/FormElements/select";
|
||||||
import MultiSelect from "@/components/FormElements/MultiSelect";
|
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||||
|
import { GlobeIcon } from "@/assets/icons";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
@@ -170,6 +171,38 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Select
|
||||||
|
{...register("type", {
|
||||||
|
required: {
|
||||||
|
message: FormErrorMessages.required,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
name="type"
|
||||||
|
label="Type"
|
||||||
|
placeholder="Select Type"
|
||||||
|
prefixIcon={<GlobeIcon />}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Individual",
|
||||||
|
value: "individual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Company",
|
||||||
|
value: "company",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Government",
|
||||||
|
value: "government",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{errors.mobile && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">{errors.mobile.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2 flex gap-6">
|
<div className="col-span-2 flex gap-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
condition: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IsVisible = ({ condition, children }: IProps) => {
|
||||||
|
if(condition) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IsVisible;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { CrossIcon } from "@/assets/icons";
|
||||||
|
import { useState, useEffect, ReactNode } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal: React.FC<ModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!mounted || !isOpen) return null;
|
||||||
|
|
||||||
|
const modalContent = (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-999999 flex items-center justify-center bg-dark/50 dark:bg-black/50"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="max-h-[90vh] min-w-75 max-w-[90vw] overflow-auto rounded-lg bg-white p-5 shadow-4 dark:bg-dark-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="float-right mb-2.5 cursor-pointer border-none bg-transparent p-0 text-2xl text-dark-5 hover:text-primary dark:text-gray-5 dark:hover:text-primary"
|
||||||
|
>
|
||||||
|
<CrossIcon />
|
||||||
|
</button>
|
||||||
|
<div className="clear-both">{children}</div>
|
||||||
|
<div className="flex gap-5">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return createPortal(modalContent, document.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@@ -46,12 +46,13 @@ export function TableFooter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableRow({
|
export const TableRow = React.forwardRef<
|
||||||
className,
|
HTMLTableRowElement,
|
||||||
...props
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
}: React.HTMLAttributes<HTMLTableRowElement>) {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:border-dark-3 dark:hover:bg-dark-2 dark:data-[state=selected]:bg-neutral-800",
|
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:border-dark-3 dark:hover:bg-dark-2 dark:data-[state=selected]:bg-neutral-800",
|
||||||
className,
|
className,
|
||||||
@@ -59,7 +60,8 @@ export function TableRow({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
export function TableHead({
|
export function TableHead({
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum TenderStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
EXPIRED = "expired",
|
||||||
|
CANCELLED = "cancelled",
|
||||||
|
AWARDED = "awarded",
|
||||||
|
DRAFT = "draft",
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ const LoginGuard = ({ children }: LoginGuardProps): React.ReactNode => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && isAuthenticated) {
|
if (!isLoading && isAuthenticated) {
|
||||||
router.push("/design-system");
|
router.push("/");
|
||||||
}
|
}
|
||||||
}, [isLoading, isAuthenticated, router]);
|
}, [isLoading, isAuthenticated, router]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ILabelValue<T = string> {
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as moment from "moment";
|
||||||
|
export const unixToDate = (unix: number) => {
|
||||||
|
return moment.unix(unix).format("DD-MM-YYYY");
|
||||||
|
};
|
||||||
|
export const getStatusColor = <T>(status: T) => {
|
||||||
|
switch (status) {
|
||||||
|
case "active":
|
||||||
|
return "bg-green-dark text-white";
|
||||||
|
case "expired":
|
||||||
|
return "bg-orange-light text-white";
|
||||||
|
case "cancelled":
|
||||||
|
return "bg-error-dark text-white";
|
||||||
|
case "awarded":
|
||||||
|
return "bg-blue-light-3";
|
||||||
|
case "draft":
|
||||||
|
return "bg-gray-3";
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user