Merge pull request 'feat(submissions): Implement submissions page with paginated table' (#30) from tender-details-page into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/30 Reviewed-by: Sina Nakhostin <s.nakhostin@ravanertebat.com>
This commit is contained in:
Generated
+30
-2
@@ -10,10 +10,12 @@
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.85.6",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react-paginate": "^7.1.4",
|
||||
"apexcharts": "^4.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"currency-symbol-map": "^5.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"flatpickr": "^4.6.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
@@ -27,6 +29,7 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-paginate": "^8.3.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^4.1.5"
|
||||
@@ -1106,7 +1109,6 @@
|
||||
"version": "19.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
|
||||
"integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1122,6 +1124,15 @@
|
||||
"@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": {
|
||||
"version": "8.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz",
|
||||
@@ -2280,9 +2291,14 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@@ -5336,6 +5352,18 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"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": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.85.6",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react-paginate": "^7.1.4",
|
||||
"apexcharts": "^4.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"currency-symbol-map": "^5.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"flatpickr": "^4.6.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
@@ -28,6 +30,7 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-paginate": "^8.3.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^4.1.5"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { use } from "react";
|
||||
import {
|
||||
useCustomersInfiniteQuery,
|
||||
useGetCustomersDetails,
|
||||
} from "../../../../hooks/queries/useCustomerQueries";
|
||||
} from "@/hooks/queries/useCustomerQueries";
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import CreateCustomer from "@/components/forms/customers/CreateCustomer";
|
||||
import Loading from "@/app/loading";
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"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 gap-2 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 gap-2 border-b pb-4">
|
||||
<ShowcaseSection title="Application Deadline">
|
||||
<span>{msToDate(data?.data.application_deadline ?? 0)}</span>
|
||||
</ShowcaseSection>
|
||||
{data?.data.submission_url && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
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) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type PropsType = {
|
||||
title: string;
|
||||
title?: string | ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
@@ -10,9 +10,11 @@ type PropsType = {
|
||||
export function ShowcaseSection({ title, children, className }: PropsType) {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
{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">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div className={cn("p-4 sm:p-6 xl:p-10", className)}>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import { formatPhoneNumber } from "@/utils/shared";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
@@ -65,7 +66,7 @@ const CompaniesTable = ({}: IProps) => {
|
||||
{company.email}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.phone}
|
||||
{formatPhoneNumber(company.phone)}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.address.country}
|
||||
|
||||
@@ -18,6 +18,9 @@ import Modal from "@/components/ui/modal";
|
||||
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||
import { toast } from "react-toastify";
|
||||
import Status from "@/components/ui/Status";
|
||||
import { unixToDate } from "@/utils/shared";
|
||||
import { custom } from "zod";
|
||||
|
||||
const CustomersTable = () => {
|
||||
const {
|
||||
isPending,
|
||||
@@ -62,10 +65,15 @@ const CustomersTable = () => {
|
||||
<TableRow key={customer.id}>
|
||||
<TableCell colSpan={100}>{customer.full_name}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.email}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.created_at}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.type}</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{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 colSpan={100}>
|
||||
{customer.companies?.length ? (
|
||||
@@ -76,6 +84,7 @@ const CustomersTable = () => {
|
||||
<span>None</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>{customer.role ?? "-"}</TableCell>
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
|
||||
@@ -75,6 +75,7 @@ const useCustomerListPresenter = () => {
|
||||
"type",
|
||||
"status",
|
||||
"company",
|
||||
"role",
|
||||
"actions",
|
||||
];
|
||||
const onConfirmDelete = () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
export function TendersSkeleton() {
|
||||
return (
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
{Array.from({ length: 20 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
@@ -22,6 +22,12 @@ export function TendersSkeleton() {
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -14,75 +14,99 @@ import {
|
||||
import { TendersSkeleton } from "./Skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
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 {
|
||||
allTenders,
|
||||
isPending,
|
||||
ref,
|
||||
isFetchingNextPage,
|
||||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useTenderListPresenter();
|
||||
const { allTenders, isPending, router, pathName, setParams } =
|
||||
useTenderListPresenter();
|
||||
|
||||
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">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<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
|
||||
</TableHead>
|
||||
</TableHead> */}
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Title
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Country Code
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Submission Url
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Status
|
||||
</TableHead>
|
||||
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Actions
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
{isPending && <TendersSkeleton />}
|
||||
<TableBody>
|
||||
{allTenders.map((item, index) =>
|
||||
{allTenders?.data.tenders?.map((item, index) =>
|
||||
!item ? (
|
||||
<h6 key={index}>No tenders were found</h6>
|
||||
) : (
|
||||
<TableRow
|
||||
ref={index === allTenders.length - 1 ? ref : null}
|
||||
key={item.id}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{/* <TableCell className="text-start" colSpan={100}>
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
</TableCell> */}
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.title}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.country_code}
|
||||
</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}>
|
||||
<Status status={item.status}>{item.status}</Status>
|
||||
</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>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
{isFetchingNextPage && <TendersSkeleton />}
|
||||
</Table>
|
||||
{(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>
|
||||
)}
|
||||
<Pagination
|
||||
currentPage={allTenders?.meta?.page ? allTenders?.meta?.page - 1 : 0}
|
||||
totalPages={allTenders?.meta?.pages ?? 1}
|
||||
onPageChange={(e) => {
|
||||
setParams((currentParams) => ({
|
||||
...currentParams,
|
||||
offset: e.selected * (allTenders?.meta?.limit ?? 10),
|
||||
limit: allTenders?.meta?.limit ?? 10,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import { useTendersInfiniteQuery } from "@/hooks/queries";
|
||||
import { useTendersQuery } from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { TCustomer } from "@/lib/api";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { z } from "zod";
|
||||
@@ -17,31 +17,18 @@ const useTenderListPresenter = () => {
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const router = useRouter();
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
const pathName = usePathname();
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useTendersInfiniteQuery(params);
|
||||
const { data, error, isPending } = useTendersQuery(params);
|
||||
|
||||
const { ref, inView } = useInView({ threshold: 0.5 });
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && (data?.pages.length ?? 0) < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, fetchNextPage, data?.pages.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const result = z.string().safeParse(debouncedSearch);
|
||||
if (result.success) {
|
||||
setParams((prevParams) => ({
|
||||
...prevParams,
|
||||
search: debouncedSearch,
|
||||
}));
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
@@ -55,20 +42,18 @@ const useTenderListPresenter = () => {
|
||||
// }
|
||||
// };
|
||||
|
||||
const allTenders = data?.pages.flatMap((page) => page.data.tenders) ?? [];
|
||||
return {
|
||||
ref,
|
||||
currentTender,
|
||||
setCurrentTender,
|
||||
search,
|
||||
handleFilterChange,
|
||||
allTenders,
|
||||
router,
|
||||
allTenders: data,
|
||||
error,
|
||||
isPending,
|
||||
hasNextPage,
|
||||
setParams,
|
||||
data,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
pathName,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
||||
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
||||
import { ShowcaseSection } from "../../Layouts/showcase-section";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import { title } from "process";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
import { CreateCustomerCredentials } from "@/lib/api";
|
||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||
import { ShowcaseSection } from "../../Layouts/showcase-section";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { GlobeIcon } from "@/assets/icons";
|
||||
import { GlobeIcon, UserIcon } from "@/assets/icons";
|
||||
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
@@ -35,8 +35,8 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
message: FormErrorMessages.maxLength(30),
|
||||
},
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9]+$/,
|
||||
message: FormErrorMessages.alphaNumeric,
|
||||
value: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
})}
|
||||
name="username"
|
||||
@@ -61,7 +61,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
}))
|
||||
: []
|
||||
}
|
||||
placeholder="Choose your interests..."
|
||||
placeholder="Select companies to assign"
|
||||
{...register("company_ids", {
|
||||
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>
|
||||
)}
|
||||
</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">
|
||||
<InputGroup
|
||||
{...register("password", {
|
||||
@@ -97,6 +117,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
message: FormErrorMessages.maxLength(128),
|
||||
},
|
||||
})}
|
||||
disabled={editMode}
|
||||
name="password"
|
||||
label="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 { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useTendersInfiniteQuery = (params?: Record<string, any>) => {
|
||||
export const useTendersQuery = (params?: Record<string, any>) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.TENDERS.READ_ALL, "read-tenders", params],
|
||||
[params],
|
||||
);
|
||||
|
||||
return useInfiniteQuery({
|
||||
return useQuery({
|
||||
queryKey,
|
||||
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
tendersService.tendersList({ ...params, offset: pageParam }),
|
||||
|
||||
initialPageParam: 0,
|
||||
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.meta) {
|
||||
const { offset, limit, total } = lastPage.meta;
|
||||
|
||||
const nextOffset = offset + limit;
|
||||
|
||||
return nextOffset < total ? nextOffset : undefined;
|
||||
}
|
||||
},
|
||||
queryFn: () => tendersService.tendersList({ ...params }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useTenderDetailQuery = (id: string) => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.TENDERS.DETAILS(id)], [id]);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => tendersService.tenderDetails(id),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const API_ENDPOINTS = {
|
||||
FLAGS: (countryCode: string) => `flags/${countryCode}`,
|
||||
POSTS: {
|
||||
READ_ALL: "posts ",
|
||||
},
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import { ApiResponse } from "../types";
|
||||
import {
|
||||
CreateCustomerCredentials,
|
||||
CustomerListResponseSchema,
|
||||
TAssignCustomerToCompanyCredentials,
|
||||
TCustomersListResponse,
|
||||
} from "../types/Customers";
|
||||
|
||||
export const customersService = {
|
||||
getCustomers: async (params?: Record<string, any>) => {
|
||||
getCustomers: async (
|
||||
params?: Record<string, any>,
|
||||
): Promise<ApiResponse<TCustomersListResponse>> => {
|
||||
try {
|
||||
const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, {
|
||||
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_ENDPOINTS } from "../endpoints";
|
||||
import { ApiResponse } from "../types";
|
||||
import { TTenderResponse } from "../types/Tenders";
|
||||
import { TTenderDetails, TTenderResponse } from "../types/Tenders";
|
||||
|
||||
export const tendersService = {
|
||||
tendersList: async (
|
||||
@@ -14,4 +14,15 @@ export const tendersService = {
|
||||
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";
|
||||
|
||||
const customersSchema = z.object({
|
||||
role: z.enum(["admin", "analyst"]).optional(),
|
||||
companies: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -75,6 +76,7 @@ const createCustomerCredentials = z.object({
|
||||
])
|
||||
.optional(),
|
||||
email: z.string().email(),
|
||||
role: z.enum(["admin", "analyst"]),
|
||||
employee_count: z.number().int().min(1).max(100000).optional(),
|
||||
first_name: z.string().min(2).max(50).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()),
|
||||
});
|
||||
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";
|
||||
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) => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
@@ -16,3 +21,23 @@ export const getStatusColor = <T>(status: T) => {
|
||||
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