From 2fa774b6bdd18c78b585a94f8e4133034b3ba1c0 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Tue, 16 Sep 2025 16:01:59 +0330 Subject: [PATCH] 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. --- package-lock.json | 10 +++ package.json | 1 + src/assets/icons.tsx | 14 ++++ src/components/Auth/SigninWithPassword.tsx | 2 +- .../FormElements/InputGroup/index.tsx | 10 ++- src/components/FormElements/select.tsx | 3 +- src/components/Layouts/sidebar/data/index.ts | 2 +- .../customers/AssignToCompanyModalContent.tsx | 67 ++++++++++++++++ src/components/Tables/customers/index.tsx | 41 +++++++++- .../customers/useCustomerListPresenter.ts | 8 +- src/components/Tables/tenders/index.tsx | 36 ++++++++- .../Tables/tenders/useTenderListPresenter.ts | 9 ++- src/components/forms/admins/CreateAdmin.tsx | 3 + .../forms/customers/CreateCustomer.tsx | 33 ++++++++ src/components/ui/IsVisible.tsx | 15 ++++ src/components/ui/modal.tsx | 77 +++++++++++++++++++ src/components/ui/table.tsx | 12 +-- src/constants/enums.ts | 7 ++ src/contexts/Login-guard.ctx.tsx | 2 +- src/types/shared.ts | 4 + src/utils/shared.ts | 18 +++++ 21 files changed, 350 insertions(+), 24 deletions(-) create mode 100644 src/components/Tables/customers/AssignToCompanyModalContent.tsx create mode 100644 src/components/ui/IsVisible.tsx create mode 100644 src/components/ui/modal.tsx create mode 100644 src/constants/enums.ts create mode 100644 src/types/shared.ts create mode 100644 src/utils/shared.ts diff --git a/package-lock.json b/package-lock.json index 3e94c7c..c120e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "flatpickr": "^4.6.13", "js-cookie": "^3.0.5", "jsvectormap": "^1.6.0", + "moment": "^2.30.1", "next": "15.1.6", "next-themes": "^0.4.4", "nextjs-toploader": "^3.7.15", @@ -4452,6 +4453,15 @@ "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": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index d757c24..0a8b43a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "flatpickr": "^4.6.13", "js-cookie": "^3.0.5", "jsvectormap": "^1.6.0", + "moment": "^2.30.1", "next": "15.1.6", "next-themes": "^0.4.4", "nextjs-toploader": "^3.7.15", diff --git a/src/assets/icons.tsx b/src/assets/icons.tsx index b94fb6e..6ec256b 100644 --- a/src/assets/icons.tsx +++ b/src/assets/icons.tsx @@ -344,6 +344,20 @@ export function GoogleIcon(props: IconProps) { ); } +export function CrossIcon(props: IconProps) { + return ( + + + + ); +} export function UserIcon(props: IconProps) { return ( diff --git a/src/components/Auth/SigninWithPassword.tsx b/src/components/Auth/SigninWithPassword.tsx index a331836..211cb40 100644 --- a/src/components/Auth/SigninWithPassword.tsx +++ b/src/components/Auth/SigninWithPassword.tsx @@ -16,7 +16,7 @@ export default function SigninWithPassword() { const { handleSubmit, control, reset, register } = useForm(); const { mutate, isPending } = useLoginQuery(); - const passwordInputRef = useRef(null); + const onSubmit: SubmitHandler = (data) => { mutate(data); reset(); diff --git a/src/components/FormElements/InputGroup/index.tsx b/src/components/FormElements/InputGroup/index.tsx index c907590..2e0505b 100644 --- a/src/components/FormElements/InputGroup/index.tsx +++ b/src/components/FormElements/InputGroup/index.tsx @@ -1,5 +1,9 @@ import { cn } from "@/lib/utils"; -import { type HTMLInputTypeAttribute, useId } from "react"; +import { + HTMLInputAutoCompleteAttribute, + type HTMLInputTypeAttribute, + useId, +} from "react"; import { type FieldErrors, type FieldValues, @@ -23,6 +27,7 @@ type InputGroupProps = { icon?: React.ReactNode; iconPosition?: "left" | "right"; height?: "sm" | "default"; + autoComplete?: HTMLInputAutoCompleteAttribute; defaultValue?: string; register?: UseFormRegister; errors?: FieldErrors; @@ -41,7 +46,7 @@ const InputGroup = ({ name, register, errors, - + autoComplete, onChange, onBlur, ref, @@ -75,6 +80,7 @@ const InputGroup = ({ placeholder={placeholder} name={name} ref={ref} + autoComplete={autoComplete} onChange={(e) => { onChange?.(e); handleChange?.(e); diff --git a/src/components/FormElements/select.tsx b/src/components/FormElements/select.tsx index 182feab..5073f97 100644 --- a/src/components/FormElements/select.tsx +++ b/src/components/FormElements/select.tsx @@ -2,7 +2,7 @@ import { ChevronUpIcon } from "@/assets/icons"; import { cn } from "@/lib/utils"; -import { useId, useState } from "react"; +import { useId, useState, type ChangeEvent } from "react"; import { type FieldErrors, type FieldValues, @@ -16,6 +16,7 @@ type PropsType = { label: string; items: { value: string; label: string }[]; prefixIcon?: React.ReactNode; + onChange?: (e: ChangeEvent) => void; className?: string; register?: UseFormRegister; errors?: FieldErrors; diff --git a/src/components/Layouts/sidebar/data/index.ts b/src/components/Layouts/sidebar/data/index.ts index 633fdf7..cd24d18 100644 --- a/src/components/Layouts/sidebar/data/index.ts +++ b/src/components/Layouts/sidebar/data/index.ts @@ -36,7 +36,7 @@ export const NAV_DATA: NavData = [ items: [], }, { - title: "companies", + title: "Companies", url: "/companies", icon: Icons.Building, items: [], diff --git a/src/components/Tables/customers/AssignToCompanyModalContent.tsx b/src/components/Tables/customers/AssignToCompanyModalContent.tsx new file mode 100644 index 0000000..61f450f --- /dev/null +++ b/src/components/Tables/customers/AssignToCompanyModalContent.tsx @@ -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 + >; +} +const AssignToCompanyModalContent: FC = ({ setCompanies }) => { + const [options, setOptions] = useState([]); + + const { data, status, isSuccess } = useCompanyFullList(); + const { handleSubmit, register } = + useForm(); + + 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 ( +
+ } + items={[ + { + label: "Individual", + value: "individual", + }, + { + label: "Company", + value: "company", + }, + { + label: "Government", + value: "government", + }, + ]} + /> + {errors.mobile && ( +

{errors.mobile.message}

+ )} + +
+
{children}
+
+ + +
+
+ + ); + + return createPortal(modalContent, document.body); +}; + +export default Modal; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index eba9266..dcebaa8 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -46,12 +46,13 @@ export function TableFooter({ ); } -export function TableRow({ - className, - ...props -}: React.HTMLAttributes) { +export const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { return ( ); -} +}); +TableRow.displayName = "TableRow"; export function TableHead({ className, diff --git a/src/constants/enums.ts b/src/constants/enums.ts new file mode 100644 index 0000000..cdaf0e0 --- /dev/null +++ b/src/constants/enums.ts @@ -0,0 +1,7 @@ +export enum TenderStatus { + ACTIVE = "active", + EXPIRED = "expired", + CANCELLED = "cancelled", + AWARDED = "awarded", + DRAFT = "draft", +} diff --git a/src/contexts/Login-guard.ctx.tsx b/src/contexts/Login-guard.ctx.tsx index 6b97f5e..c4453c0 100644 --- a/src/contexts/Login-guard.ctx.tsx +++ b/src/contexts/Login-guard.ctx.tsx @@ -14,7 +14,7 @@ const LoginGuard = ({ children }: LoginGuardProps): React.ReactNode => { useEffect(() => { if (!isLoading && isAuthenticated) { - router.push("/design-system"); + router.push("/"); } }, [isLoading, isAuthenticated, router]); diff --git a/src/types/shared.ts b/src/types/shared.ts new file mode 100644 index 0000000..97d4d70 --- /dev/null +++ b/src/types/shared.ts @@ -0,0 +1,4 @@ +export interface ILabelValue { + label: string; + value: T; +} diff --git a/src/utils/shared.ts b/src/utils/shared.ts new file mode 100644 index 0000000..d78c17d --- /dev/null +++ b/src/utils/shared.ts @@ -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 = (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"; + } +};