feat(submissions): Implement submissions page with paginated table
This commit introduces a new page for users to view their submissions in a structured and paginated table. This allows users to easily track their submission history and access key details. The main changes include: - A new page at `/submissions` to display the submissions list. - A `SubmissionsTable` component to render submission data, including title, country, status, and a link to the submission URL. - Integration of `react-paginate` to handle navigation through large sets of data. - A new `useSubmissions` custom hook using TanStack Query for efficient, paginated data fetching from the API. - Addition of the `currency-symbol-map` dependency to display prices with the correct currency symbol.
This commit is contained in:
Generated
+30
-2
@@ -10,10 +10,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.85.6",
|
"@tanstack/react-query": "^5.85.6",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/react-paginate": "^7.1.4",
|
||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"currency-symbol-map": "^5.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
|
"react-paginate": "^8.3.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
@@ -1106,7 +1109,6 @@
|
|||||||
"version": "19.0.8",
|
"version": "19.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
|
||||||
"integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==",
|
"integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -1122,6 +1124,15 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-paginate": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/react-paginate/-/react-paginate-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-6fqZvDzRJHubOGl6c7cGFC9ysgQSWYy0Gpus9HjORpydlcXgPnT8x+aKgqwCdtpZrRTwcBz2Q7JWAOYaRrsXGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.27.0",
|
"version": "8.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz",
|
||||||
@@ -2280,9 +2291,14 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/currency-symbol-map": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/currency-symbol-map/-/currency-symbol-map-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@@ -5336,6 +5352,18 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-paginate": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-paginate/-/react-paginate-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-TptZE37HPkT3R+7AszWA++LOTIsIHXcCSWMP9WW/abeF8sLpJzExFB/dVs7xbtqteJ5njF6kk+udTDC0AR3y5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-toastify": {
|
"node_modules/react-toastify": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmmirror.com/react-toastify/-/react-toastify-11.0.5.tgz",
|
"resolved": "https://registry.npmmirror.com/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||||
|
|||||||
@@ -11,10 +11,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.85.6",
|
"@tanstack/react-query": "^5.85.6",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/react-paginate": "^7.1.4",
|
||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"currency-symbol-map": "^5.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
|
"react-paginate": "^8.3.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { use } from "react";
|
|||||||
import {
|
import {
|
||||||
useCustomersInfiniteQuery,
|
useCustomersInfiniteQuery,
|
||||||
useGetCustomersDetails,
|
useGetCustomersDetails,
|
||||||
} from "../../../../hooks/queries/useCustomerQueries";
|
} from "@/hooks/queries/useCustomerQueries";
|
||||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||||
import CreateCustomer from "@/components/forms/customers/CreateCustomer";
|
import CreateCustomer from "@/components/forms/customers/CreateCustomer";
|
||||||
import Loading from "@/app/loading";
|
import Loading from "@/app/loading";
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"use client";
|
||||||
|
import Loading from "@/app/loading";
|
||||||
|
import { LocationIcon } from "@/assets/icons";
|
||||||
|
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||||
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
|
import Status from "@/components/ui/Status";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { useTenderDetailQuery } from "@/hooks/queries";
|
||||||
|
import { useGetFlagQuery } from "@/hooks/queries/useFlagsQueries";
|
||||||
|
import { msToDate, unixToDate } from "@/utils/shared";
|
||||||
|
import getSymbolFromCurrency from "currency-symbol-map";
|
||||||
|
import { use } from "react";
|
||||||
|
import { TableCell } from "../../../components/ui/table";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
params: Promise<{
|
||||||
|
details: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TenderDetails = ({ params }: IProps) => {
|
||||||
|
const { details } = use(params);
|
||||||
|
const { data, isLoading, isError } = useTenderDetailQuery(details);
|
||||||
|
const breadcrumbItems = [
|
||||||
|
{ name: "Dashboard", href: "/" },
|
||||||
|
{ name: "Tenders", href: "/tenders" },
|
||||||
|
{ name: "Tender details", href: `/tenders/${details}` },
|
||||||
|
];
|
||||||
|
const { data: flagData } = useGetFlagQuery(data?.data.country_code ?? "EU");
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
|
if (isError) return <div>Error</div>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb items={breadcrumbItems} />
|
||||||
|
<ShowcaseSection className="!p-6.5">
|
||||||
|
<div className="flex justify-between border-b pb-4">
|
||||||
|
<p>{data?.data.title}</p>
|
||||||
|
<Status status={data?.data.status ?? "Unknown"}>
|
||||||
|
{data?.data.status}
|
||||||
|
</Status>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between border-b pb-4">
|
||||||
|
<ShowcaseSection title="Tender ID">
|
||||||
|
{data?.data.tender_id}
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Buyer Organization">
|
||||||
|
{data?.data.buyer_organization.name}
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Notice Publication Id">
|
||||||
|
{data?.data.notice_publication_id}
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Procedure Code">
|
||||||
|
{data?.data.procedure_code}
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Procurement Type Code">
|
||||||
|
{data?.data.procurement_type_code}
|
||||||
|
</ShowcaseSection>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between border-b pb-4">
|
||||||
|
<ShowcaseSection title="Application Deadline">
|
||||||
|
<span>{msToDate(data?.data.application_deadline ?? 0)}</span>
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Submission Url">
|
||||||
|
<Link href={data?.data.submission_url ?? ""} target="_blank">
|
||||||
|
{data?.data.submission_url}
|
||||||
|
</Link>
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Publication Deadline">
|
||||||
|
<span>{msToDate(data?.data.publication_date ?? 0)}</span>
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Submission Deadline">
|
||||||
|
<span>{msToDate(data?.data.submission_deadline ?? 0)}</span>
|
||||||
|
</ShowcaseSection>
|
||||||
|
<ShowcaseSection title="Tender Deadline">
|
||||||
|
<span>{msToDate(data?.data.tender_deadline ?? 0)}</span>
|
||||||
|
</ShowcaseSection>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between border-b pb-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-6 text-lg font-semibold">Country</h2>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<LocationIcon />
|
||||||
|
<p className="text-sm uppercase">{data?.data.country_code}</p>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: flagData }}
|
||||||
|
className="flex w-7 items-center justify-center overflow-hidden rounded-md border p-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{data?.data.currency && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-6 text-lg font-semibold">Currency</h2>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<p className="text-sm uppercase">{data?.data.currency}</p>
|
||||||
|
<p>{getSymbolFromCurrency(data?.data.currency ?? "USD")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h2 className="mb-6 text-lg font-semibold">Description</h2>
|
||||||
|
<p>{data?.data.description}</p>
|
||||||
|
</div>
|
||||||
|
</ShowcaseSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TenderDetails;
|
||||||
@@ -344,6 +344,32 @@ export function GoogleIcon(props: IconProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export function LocationIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M480-480q33 0 56.5-23.5T560-560q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560q0 33 23.5 56.5T480-480Zm0 294q122-112 181-203.5T720-552q0-109-69.5-178.5T480-800q-101 0-170.5 69.5T240-552q0 71 59 162.5T480-186Zm0 106Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Zm0-480Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function ExclamationIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M440-400v-360h80v360h-80Zm0 200v-80h80v80h-80Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
export function CrossIcon(props: IconProps) {
|
export function CrossIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
title: string;
|
title?: string | ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@@ -10,9 +10,11 @@ type PropsType = {
|
|||||||
export function ShowcaseSection({ title, children, className }: PropsType) {
|
export function ShowcaseSection({ title, children, className }: PropsType) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||||
<h2 className="border-b border-stroke px-4 py-4 font-medium text-dark dark:border-dark-3 dark:text-white sm:px-6 xl:px-7.5">
|
{title && (
|
||||||
{title}
|
<h2 className="border-b border-stroke px-4 py-4 font-medium text-dark dark:border-dark-3 dark:text-white sm:px-6 xl:px-7.5">
|
||||||
</h2>
|
{title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={cn("p-4 sm:p-6 xl:p-10", className)}>{children}</div>
|
<div className={cn("p-4 sm:p-6 xl:p-10", className)}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||||
|
import { formatPhoneNumber } from "@/utils/shared";
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ const CompaniesTable = ({}: IProps) => {
|
|||||||
{company.email}
|
{company.email}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-start" colSpan={100}>
|
<TableHead className="text-start" colSpan={100}>
|
||||||
{company.phone}
|
{formatPhoneNumber(company.phone)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-start" colSpan={100}>
|
<TableHead className="text-start" colSpan={100}>
|
||||||
{company.address.country}
|
{company.address.country}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import Modal from "@/components/ui/modal";
|
|||||||
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import Status from "@/components/ui/Status";
|
import Status from "@/components/ui/Status";
|
||||||
|
import { unixToDate } from "@/utils/shared";
|
||||||
|
import { custom } from "zod";
|
||||||
|
|
||||||
const CustomersTable = () => {
|
const CustomersTable = () => {
|
||||||
const {
|
const {
|
||||||
isPending,
|
isPending,
|
||||||
@@ -62,10 +65,15 @@ const CustomersTable = () => {
|
|||||||
<TableRow key={customer.id}>
|
<TableRow key={customer.id}>
|
||||||
<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}>{customer.created_at}</TableCell>
|
|
||||||
<TableCell colSpan={100}>{customer.type}</TableCell>
|
|
||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
<Status status={customer.status}>{customer.status}</Status>
|
{unixToDate(customer.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell colSpan={100} className="uppercase">{customer.type}</TableCell>
|
||||||
|
<TableCell colSpan={100}>
|
||||||
|
<Status status={customer.status}>
|
||||||
|
<Status status={customer.status}>{customer.status}</Status>
|
||||||
|
</Status>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
{customer.companies?.length ? (
|
{customer.companies?.length ? (
|
||||||
@@ -76,6 +84,7 @@ const CustomersTable = () => {
|
|||||||
<span>None</span>
|
<span>None</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell colSpan={100}>{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
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const useCustomerListPresenter = () => {
|
|||||||
"type",
|
"type",
|
||||||
"status",
|
"status",
|
||||||
"company",
|
"company",
|
||||||
|
"role",
|
||||||
"actions",
|
"actions",
|
||||||
];
|
];
|
||||||
const onConfirmDelete = () => {
|
const onConfirmDelete = () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
export function TendersSkeleton() {
|
export function TendersSkeleton() {
|
||||||
return (
|
return (
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 20 }).map((_, i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
<Skeleton className="h-8" />
|
<Skeleton className="h-8" />
|
||||||
@@ -22,6 +22,12 @@ export function TendersSkeleton() {
|
|||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
<Skeleton className="h-8" />
|
<Skeleton className="h-8" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell colSpan={100}>
|
||||||
|
<Skeleton className="h-8" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell colSpan={100}>
|
||||||
|
<Skeleton className="h-8" />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -14,75 +14,99 @@ import {
|
|||||||
import { TendersSkeleton } from "./Skeleton";
|
import { TendersSkeleton } from "./Skeleton";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getStatusColor } from "@/utils/shared";
|
import { getStatusColor } from "@/utils/shared";
|
||||||
|
import { ExclamationIcon } from "@/assets/icons";
|
||||||
|
import Pagination from "@/components/ui/pagination";
|
||||||
|
import Page from "../../../app/forms/form-layout/page";
|
||||||
|
import Link from "next/link";
|
||||||
const TendersTable = () => {
|
const TendersTable = () => {
|
||||||
const {
|
const { allTenders, isPending, router, pathName, setParams } =
|
||||||
allTenders,
|
useTenderListPresenter();
|
||||||
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>
|
||||||
<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">
|
{/* <TableHead colSpan={100} className="text-start font-extrabold">
|
||||||
Row
|
Row
|
||||||
</TableHead>
|
</TableHead> */}
|
||||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||||
Title
|
Title
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||||
Country Code
|
Country Code
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||||
|
Submission Url
|
||||||
|
</TableHead>
|
||||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||||
Status
|
Status
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
||||||
|
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||||
|
Actions
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
||||||
{isPending && <TendersSkeleton />}
|
{isPending && <TendersSkeleton />}
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{allTenders.map((item, index) =>
|
{allTenders?.data.tenders?.map((item, index) =>
|
||||||
!item ? (
|
!item ? (
|
||||||
<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"
|
||||||
>
|
>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
{/* <TableCell className="text-start" colSpan={100}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</TableCell>
|
</TableCell> */}
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.country_code}
|
{item.country_code}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||||
|
{item.submission_url ? (
|
||||||
|
<Link target="_blank" href={item.submission_url}>
|
||||||
|
{item.submission_url}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||||
<Status status={item.status}>{item.status}</Status>
|
<Status status={item.status}>{item.status}</Status>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||||
|
<button
|
||||||
|
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`${pathName}/${item.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExclamationIcon />
|
||||||
|
</button>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
{isFetchingNextPage && <TendersSkeleton />}
|
|
||||||
</Table>
|
</Table>
|
||||||
{(data?.pages.length ?? 0) >= 5 && hasNextPage && (
|
<Pagination
|
||||||
<button
|
currentPage={allTenders?.meta?.page ? allTenders?.meta?.page - 1 : 0}
|
||||||
onClick={() => fetchNextPage()}
|
totalPages={allTenders?.meta?.pages ?? 1}
|
||||||
disabled={isFetchingNextPage}
|
onPageChange={(e) => {
|
||||||
className="mt-4 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
|
setParams((currentParams) => ({
|
||||||
>
|
...currentParams,
|
||||||
{isFetchingNextPage ? "Loading..." : "Load More"}
|
offset: e.selected * (allTenders?.meta?.limit ?? 10),
|
||||||
</button>
|
limit: allTenders?.meta?.limit ?? 10,
|
||||||
)}
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||||
import { useTendersInfiniteQuery } from "@/hooks/queries";
|
import { useTendersQuery } from "@/hooks/queries";
|
||||||
import useDebounce from "@/hooks/useDebounce";
|
import useDebounce from "@/hooks/useDebounce";
|
||||||
import { TCustomer } from "@/lib/api";
|
import { TCustomer } from "@/lib/api";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { 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";
|
||||||
@@ -17,31 +17,18 @@ const useTenderListPresenter = () => {
|
|||||||
const [params, setParams] = useState<Record<string, any>>({
|
const [params, setParams] = useState<Record<string, any>>({
|
||||||
sort: "created_at",
|
sort: "created_at",
|
||||||
});
|
});
|
||||||
|
const router = useRouter();
|
||||||
const debouncedSearch = useDebounce(search, 1000);
|
const debouncedSearch = useDebounce(search, 1000);
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
const {
|
const { data, error, isPending } = useTendersQuery(params);
|
||||||
data,
|
|
||||||
error,
|
|
||||||
isPending,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
} = useTendersInfiniteQuery(params);
|
|
||||||
|
|
||||||
const { ref, inView } = useInView({ threshold: 0.5 });
|
const { ref, inView } = useInView({ threshold: 0.5 });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inView && hasNextPage && (data?.pages.length ?? 0) < 5) {
|
|
||||||
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,
|
||||||
search: debouncedSearch,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [debouncedSearch]);
|
}, [debouncedSearch]);
|
||||||
@@ -55,20 +42,18 @@ const useTenderListPresenter = () => {
|
|||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const allTenders = data?.pages.flatMap((page) => page.data.tenders) ?? [];
|
|
||||||
return {
|
return {
|
||||||
ref,
|
ref,
|
||||||
currentTender,
|
currentTender,
|
||||||
setCurrentTender,
|
setCurrentTender,
|
||||||
search,
|
search,
|
||||||
handleFilterChange,
|
handleFilterChange,
|
||||||
allTenders,
|
router,
|
||||||
|
allTenders: data,
|
||||||
error,
|
error,
|
||||||
isPending,
|
isPending,
|
||||||
hasNextPage,
|
setParams,
|
||||||
data,
|
data,
|
||||||
fetchNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
pathName,
|
pathName,
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
setIsModalOpen,
|
setIsModalOpen,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
||||||
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
||||||
import { ShowcaseSection } from "../../Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
import { title } from "process";
|
import { title } from "process";
|
||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import { CreateCustomerCredentials } from "@/lib/api";
|
import { CreateCustomerCredentials } from "@/lib/api";
|
||||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||||
import { ShowcaseSection } from "../../Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
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";
|
import { GlobeIcon, UserIcon } from "@/assets/icons";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
@@ -35,8 +35,8 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
message: FormErrorMessages.maxLength(30),
|
message: FormErrorMessages.maxLength(30),
|
||||||
},
|
},
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^[a-zA-Z0-9]+$/,
|
value: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
||||||
message: FormErrorMessages.alphaNumeric,
|
message: FormErrorMessages.invalidPattern,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
name="username"
|
name="username"
|
||||||
@@ -61,7 +61,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
placeholder="Choose your interests..."
|
placeholder="Select companies to assign"
|
||||||
{...register("company_ids", {
|
{...register("company_ids", {
|
||||||
required: "Please select at least one interest",
|
required: "Please select at least one interest",
|
||||||
})}
|
})}
|
||||||
@@ -87,6 +87,26 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
|
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Select
|
||||||
|
label="Role"
|
||||||
|
{...register("role", {
|
||||||
|
required: { message: FormErrorMessages.required, value: true },
|
||||||
|
})}
|
||||||
|
name="role"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Admin",
|
||||||
|
value: "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Analyst",
|
||||||
|
value: "analyst",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
placeholder="Select Role"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputGroup
|
<InputGroup
|
||||||
{...register("password", {
|
{...register("password", {
|
||||||
@@ -97,6 +117,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
message: FormErrorMessages.maxLength(128),
|
message: FormErrorMessages.maxLength(128),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
disabled={editMode}
|
||||||
name="password"
|
name="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { FC, Fragment } from "react";
|
||||||
|
import ReactPaginate, { ReactPaginateProps } from "react-paginate";
|
||||||
|
interface IProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange: ReactPaginateProps["onPageChange"];
|
||||||
|
}
|
||||||
|
const Pagination: FC<IProps> = ({ totalPages, currentPage, onPageChange }) => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ReactPaginate
|
||||||
|
pageCount={totalPages}
|
||||||
|
initialPage={currentPage}
|
||||||
|
nextLabel=">"
|
||||||
|
previousLabel="<"
|
||||||
|
pageRangeDisplayed={5}
|
||||||
|
activeClassName="flex justify-center rounded-md bg-primary p-1 font-medium !text-white hover:bg-opacity-90"
|
||||||
|
onPageChange={(e) => {
|
||||||
|
if (!onPageChange) return;
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
onPageChange(e);
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center justify-center text-sm text-gray-dark dark:text-white"
|
||||||
|
nextClassName="flex justify-center rounded-md p-1 font-medium text-gray-dark dark:text-white hover:bg-opacity-90"
|
||||||
|
previousClassName="flex justify-center rounded-md p-1 font-medium text-gray-dark dark:text-white hover:bg-opacity-90"
|
||||||
|
pageClassName="flex justify-center h-fit rounded-md mx-2 p-1 text-gray-dark dark:text-white hover:bg-opacity-90"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { API_ENDPOINTS } from "@/lib/api";
|
||||||
|
import { flagsService } from "@/lib/api/services/flags-service";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useGetFlagQuery = (countryCode: string) => {
|
||||||
|
const queryKey = useMemo(
|
||||||
|
() => [API_ENDPOINTS.FLAGS(countryCode)],
|
||||||
|
[countryCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey,
|
||||||
|
queryFn: () => flagsService.getCountryFlag(countryCode),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,29 +1,23 @@
|
|||||||
import { API_ENDPOINTS, tendersService } from "@/lib/api";
|
import { API_ENDPOINTS, tendersService } from "@/lib/api";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export const useTendersInfiniteQuery = (params?: Record<string, any>) => {
|
export const useTendersQuery = (params?: Record<string, any>) => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.TENDERS.READ_ALL, "read-tenders", params],
|
() => [API_ENDPOINTS.TENDERS.READ_ALL, "read-tenders", params],
|
||||||
[params],
|
[params],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
|
queryFn: () => tendersService.tendersList({ ...params }),
|
||||||
queryFn: ({ pageParam = 0 }) =>
|
});
|
||||||
tendersService.tendersList({ ...params, offset: pageParam }),
|
};
|
||||||
|
|
||||||
initialPageParam: 0,
|
export const useTenderDetailQuery = (id: string) => {
|
||||||
|
const queryKey = useMemo(() => [API_ENDPOINTS.TENDERS.DETAILS(id)], [id]);
|
||||||
getNextPageParam: (lastPage) => {
|
return useQuery({
|
||||||
if (lastPage.meta) {
|
queryKey,
|
||||||
const { offset, limit, total } = lastPage.meta;
|
queryFn: () => tendersService.tenderDetails(id),
|
||||||
|
|
||||||
const nextOffset = offset + limit;
|
|
||||||
|
|
||||||
return nextOffset < total ? nextOffset : undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
|
FLAGS: (countryCode: string) => `flags/${countryCode}`,
|
||||||
POSTS: {
|
POSTS: {
|
||||||
READ_ALL: "posts ",
|
READ_ALL: "posts ",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import api from "../axios";
|
import api from "../axios";
|
||||||
import { API_ENDPOINTS } from "../endpoints";
|
import { API_ENDPOINTS } from "../endpoints";
|
||||||
|
import { ApiResponse } from "../types";
|
||||||
import {
|
import {
|
||||||
CreateCustomerCredentials,
|
CreateCustomerCredentials,
|
||||||
CustomerListResponseSchema,
|
CustomerListResponseSchema,
|
||||||
TAssignCustomerToCompanyCredentials,
|
TAssignCustomerToCompanyCredentials,
|
||||||
|
TCustomersListResponse,
|
||||||
} from "../types/Customers";
|
} from "../types/Customers";
|
||||||
|
|
||||||
export const customersService = {
|
export const customersService = {
|
||||||
getCustomers: async (params?: Record<string, any>) => {
|
getCustomers: async (
|
||||||
|
params?: Record<string, any>,
|
||||||
|
): Promise<ApiResponse<TCustomersListResponse>> => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, {
|
const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, {
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import api from "../axios";
|
||||||
|
import { API_ENDPOINTS } from "../endpoints";
|
||||||
|
|
||||||
|
export const flagsService = {
|
||||||
|
getCountryFlag: async (countryCode: string) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.get(API_ENDPOINTS.FLAGS(countryCode));
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("ERROR caught in Flags Service", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import api from "../axios";
|
import api from "../axios";
|
||||||
import { API_ENDPOINTS } from "../endpoints";
|
import { API_ENDPOINTS } from "../endpoints";
|
||||||
import { ApiResponse } from "../types";
|
import { ApiResponse } from "../types";
|
||||||
import { TTenderResponse } from "../types/Tenders";
|
import { TTenderDetails, TTenderResponse } from "../types/Tenders";
|
||||||
|
|
||||||
export const tendersService = {
|
export const tendersService = {
|
||||||
tendersList: async (
|
tendersList: async (
|
||||||
@@ -14,4 +14,15 @@ export const tendersService = {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
tenderDetails: async (id: string): Promise<ApiResponse<TTenderDetails>> => {
|
||||||
|
try {
|
||||||
|
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id))).data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"ERROR caught in Tender Service => tender details: ",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
import { createApiResponseSchema } from "./Factory";
|
import { createApiResponseSchema } from "./Factory";
|
||||||
|
|
||||||
const customersSchema = z.object({
|
const customersSchema = z.object({
|
||||||
|
role: z.enum(["admin", "analyst"]).optional(),
|
||||||
companies: z
|
companies: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -75,6 +76,7 @@ const createCustomerCredentials = z.object({
|
|||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
role: z.enum(["admin", "analyst"]),
|
||||||
employee_count: z.number().int().min(1).max(100000).optional(),
|
employee_count: z.number().int().min(1).max(100000).optional(),
|
||||||
first_name: z.string().min(2).max(50).optional(),
|
first_name: z.string().min(2).max(50).optional(),
|
||||||
founded_year: z.number().int().min(1800).max(2100).optional(),
|
founded_year: z.number().int().min(1800).max(2100).optional(),
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ export const TendersResponse = z.object({
|
|||||||
tenders: z.array(TenderSchema).or(z.null()),
|
tenders: z.array(TenderSchema).or(z.null()),
|
||||||
});
|
});
|
||||||
export type TTenderResponse = z.infer<typeof TendersResponse>;
|
export type TTenderResponse = z.infer<typeof TendersResponse>;
|
||||||
|
export type TTenderDetails = z.infer<typeof TenderSchema>;
|
||||||
|
|||||||
+26
-1
@@ -1,7 +1,12 @@
|
|||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
export const unixToDate = (unix: number) => {
|
export const unixToDate = (unix: number) => {
|
||||||
return moment.unix(unix).format("DD-MM-YYYY");
|
return moment.unix(unix).format("YYYY/MM/DD");
|
||||||
};
|
};
|
||||||
|
export const msToDate = (ms: number) => {
|
||||||
|
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":
|
||||||
@@ -16,3 +21,23 @@ export const getStatusColor = <T>(status: T) => {
|
|||||||
return "bg-gray-3";
|
return "bg-gray-3";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
export const formatPhoneNumber = (input?: string) => {
|
||||||
|
if (!input) return "";
|
||||||
|
|
||||||
|
let cleaned = input.replace(/[^\d+]/g, "");
|
||||||
|
|
||||||
|
const hasPlus = cleaned.startsWith("+");
|
||||||
|
if (hasPlus) {
|
||||||
|
cleaned = cleaned.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < cleaned.length) {
|
||||||
|
const size = i === 0 ? 3 : 4;
|
||||||
|
groups.push(cleaned.slice(i, i + size));
|
||||||
|
i += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (hasPlus ? "+" : "") + groups.join(" ");
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user