This commit is contained in:
@@ -5,7 +5,6 @@ import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
|||||||
import IsVisible from "@/components/ui/IsVisible";
|
import IsVisible from "@/components/ui/IsVisible";
|
||||||
import ListHeader from "@/components/ui/ListHeader";
|
import ListHeader from "@/components/ui/ListHeader";
|
||||||
import ListWrapper from "@/components/ui/ListWrapper";
|
import ListWrapper from "@/components/ui/ListWrapper";
|
||||||
import Pagination from "@/components/ui/pagination";
|
|
||||||
import Status from "@/components/ui/Status";
|
import Status from "@/components/ui/Status";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -49,7 +48,8 @@ const TendersTable = () => {
|
|||||||
navigateToTenderDetails,
|
navigateToTenderDetails,
|
||||||
navigateToTenderFeedback,
|
navigateToTenderFeedback,
|
||||||
handlePaginationChange,
|
handlePaginationChange,
|
||||||
buildWindowedPageLabel,
|
hasNext,
|
||||||
|
hasPrevious,
|
||||||
getRowNumber,
|
getRowNumber,
|
||||||
formatDateTimeCell,
|
formatDateTimeCell,
|
||||||
pagination,
|
pagination,
|
||||||
@@ -204,16 +204,37 @@ const TendersTable = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<IsVisible condition={shouldShowPagination}>
|
<IsVisible condition={shouldShowPagination}>
|
||||||
<Pagination
|
<nav
|
||||||
key={`pagination-tenders-${absoluteCurrentPage}`}
|
className="mt-2 flex w-full items-center justify-center gap-3 text-sm"
|
||||||
currentPage={pagination.currentPage}
|
aria-label="Tender pagination"
|
||||||
totalPages={pagination.totalPages}
|
>
|
||||||
onPageChange={handlePaginationChange}
|
<button
|
||||||
pageRangeDisplayed={3}
|
type="button"
|
||||||
marginPagesDisplayed={1}
|
disabled={!hasPrevious}
|
||||||
pageLabelBuilder={buildWindowedPageLabel}
|
onClick={() =>
|
||||||
variant="tenders"
|
handlePaginationChange({ selected: absoluteCurrentPage - 1 })
|
||||||
/>
|
}
|
||||||
|
className="shadow-theme-xs rounded-xl border border-stroke/80 bg-white/70 px-4 py-2 font-semibold text-dark transition hover:border-primary/35 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40 dark:border-dark-3 dark:bg-dark-2/70 dark:text-white"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className="text-gray-600 dark:text-gray-300"
|
||||||
|
aria-current="page"
|
||||||
|
>
|
||||||
|
Page {pagination.currentPage + 1}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!hasNext}
|
||||||
|
onClick={() =>
|
||||||
|
handlePaginationChange({ selected: absoluteCurrentPage + 1 })
|
||||||
|
}
|
||||||
|
className="shadow-theme-xs rounded-xl border border-stroke/80 bg-white/70 px-4 py-2 font-semibold text-dark transition hover:border-primary/35 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40 dark:border-dark-3 dark:bg-dark-2/70 dark:text-white"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
</IsVisible>
|
</IsVisible>
|
||||||
</ListWrapper>
|
</ListWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,14 +8,20 @@ import { TCustomer } from "@/lib/api";
|
|||||||
import {
|
import {
|
||||||
buildQueryString,
|
buildQueryString,
|
||||||
deleteEmptyKeys,
|
deleteEmptyKeys,
|
||||||
getPaginatedRowNumber,
|
|
||||||
isDateRangeActive,
|
isDateRangeActive,
|
||||||
unixToDate,
|
unixToDate,
|
||||||
} from "@/utils/shared";
|
} from "@/utils/shared";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import { useIsMutating } from "@tanstack/react-query";
|
import { useIsMutating } from "@tanstack/react-query";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
const COMMA_SEPARATED_FILTER_KEYS = new Set([
|
const COMMA_SEPARATED_FILTER_KEYS = new Set([
|
||||||
@@ -356,11 +362,10 @@ const useTenderListPresenter = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newParams: Record<string, any> = {
|
const newParams: Record<string, any> = {
|
||||||
...apiDefaultParams,
|
...apiDefaultParams,
|
||||||
offset: 0,
|
limit: 20,
|
||||||
limit: 10,
|
|
||||||
};
|
};
|
||||||
for (const key of searchParams.keys()) {
|
for (const key of searchParams.keys()) {
|
||||||
if (key === "cpv_codes") {
|
if (key === "cpv_codes" || key === "include_total" || key === "cursor") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
newParams[key] = searchParams.get(key);
|
newParams[key] = searchParams.get(key);
|
||||||
@@ -376,53 +381,21 @@ const useTenderListPresenter = () => {
|
|||||||
|
|
||||||
const { data, error, isPending, isError } = useTendersQuery(params);
|
const { data, error, isPending, isError } = useTendersQuery(params);
|
||||||
|
|
||||||
const { pagination, handlePaginationChange, buildFirstPageParams } =
|
const {
|
||||||
useHybridPagination({
|
pagination,
|
||||||
meta: data?.meta,
|
handlePaginationChange,
|
||||||
params,
|
buildFirstPageParams,
|
||||||
setParams,
|
hasNext,
|
||||||
isError,
|
hasPrevious,
|
||||||
error,
|
} = useHybridPagination({
|
||||||
});
|
meta: data?.meta,
|
||||||
|
params,
|
||||||
|
setParams,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
const MAX_PAGE_JUMP = 10;
|
const shouldShowPagination = !isPending && (hasPrevious || hasNext);
|
||||||
const paginationWindow = useMemo(() => {
|
|
||||||
const windowStart = Math.max(0, pagination.currentPage - MAX_PAGE_JUMP);
|
|
||||||
const windowEnd = Math.min(
|
|
||||||
Math.max(pagination.totalPages - 1, 0),
|
|
||||||
pagination.currentPage + MAX_PAGE_JUMP,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
windowStart,
|
|
||||||
virtualCurrent: pagination.currentPage - windowStart,
|
|
||||||
virtualTotal: Math.max(windowEnd - windowStart + 1, 1),
|
|
||||||
};
|
|
||||||
}, [pagination.currentPage, pagination.totalPages]);
|
|
||||||
|
|
||||||
const handleWindowedPaginationChange = useCallback(
|
|
||||||
(e: { selected: number }) => {
|
|
||||||
handlePaginationChange({
|
|
||||||
selected: e.selected + paginationWindow.windowStart,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[handlePaginationChange, paginationWindow.windowStart],
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildWindowedPageLabel = useCallback(
|
|
||||||
(virtualPage: number) => String(virtualPage + paginationWindow.windowStart),
|
|
||||||
[paginationWindow.windowStart],
|
|
||||||
);
|
|
||||||
|
|
||||||
const windowedPagination = useMemo(
|
|
||||||
() => ({
|
|
||||||
currentPage: paginationWindow.virtualCurrent,
|
|
||||||
totalPages: paginationWindow.virtualTotal,
|
|
||||||
}),
|
|
||||||
[paginationWindow.virtualCurrent, paginationWindow.virtualTotal],
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldShowPagination =
|
|
||||||
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
|
|
||||||
|
|
||||||
const tendersList = data?.data?.tenders ?? [];
|
const tendersList = data?.data?.tenders ?? [];
|
||||||
|
|
||||||
@@ -449,12 +422,11 @@ const useTenderListPresenter = () => {
|
|||||||
|
|
||||||
const getRowNumber = useCallback(
|
const getRowNumber = useCallback(
|
||||||
(index: number) =>
|
(index: number) =>
|
||||||
getPaginatedRowNumber({
|
pagination.currentPage *
|
||||||
index,
|
(data?.meta?.limit ?? Number(params.limit) ?? 20) +
|
||||||
page: data?.meta?.page,
|
index +
|
||||||
limit: data?.meta?.limit,
|
1,
|
||||||
}),
|
[data?.meta?.limit, pagination.currentPage, params.limit],
|
||||||
[data?.meta?.limit, data?.meta?.page],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatDateTimeCell = useCallback((unix: number) => {
|
const formatDateTimeCell = useCallback((unix: number) => {
|
||||||
@@ -541,7 +513,7 @@ const useTenderListPresenter = () => {
|
|||||||
const clearAllFilters = useCallback(() => {
|
const clearAllFilters = useCallback(() => {
|
||||||
const cleared = buildFirstPageParams({
|
const cleared = buildFirstPageParams({
|
||||||
...apiDefaultParams,
|
...apiDefaultParams,
|
||||||
limit: 10,
|
limit: 20,
|
||||||
});
|
});
|
||||||
filterFormReset(mergeTenderFilterFormState(cleared));
|
filterFormReset(mergeTenderFilterFormState(cleared));
|
||||||
router.push(pathName);
|
router.push(pathName);
|
||||||
@@ -629,11 +601,12 @@ const useTenderListPresenter = () => {
|
|||||||
tendersList,
|
tendersList,
|
||||||
navigateToTenderDetails,
|
navigateToTenderDetails,
|
||||||
navigateToTenderFeedback,
|
navigateToTenderFeedback,
|
||||||
handlePaginationChange: handleWindowedPaginationChange,
|
handlePaginationChange,
|
||||||
buildWindowedPageLabel,
|
hasNext,
|
||||||
|
hasPrevious,
|
||||||
getRowNumber,
|
getRowNumber,
|
||||||
formatDateTimeCell,
|
formatDateTimeCell,
|
||||||
pagination: windowedPagination,
|
pagination,
|
||||||
absoluteCurrentPage: pagination.currentPage,
|
absoluteCurrentPage: pagination.currentPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { API_ENDPOINTS, tendersService } from "@/lib/api";
|
import { API_ENDPOINTS, tendersService } from "@/lib/api";
|
||||||
|
import { TTenderListParams } from "@/lib/api/types/Tenders";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const useTendersQuery = (params?: Record<string, any>) => {
|
export const useTendersQuery = (params?: TTenderListParams) => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.TENDERS.READ_ALL, "read-tenders", params],
|
() => [API_ENDPOINTS.TENDERS.READ_ALL, "read-tenders", params],
|
||||||
[params],
|
[params],
|
||||||
@@ -15,7 +16,10 @@ export const useTendersQuery = (params?: Record<string, any>) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTenderDetailQuery = (id: string, params?: Record<string, any>) => {
|
export const useTenderDetailQuery = (
|
||||||
|
id: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
) => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.TENDERS.DETAILS(id), params],
|
() => [API_ENDPOINTS.TENDERS.DETAILS(id), params],
|
||||||
[id, params],
|
[id, params],
|
||||||
@@ -52,13 +56,10 @@ export const useGetTenderDocumentsQuery = (
|
|||||||
id: string,
|
id: string,
|
||||||
options?: { enabled?: boolean },
|
options?: { enabled?: boolean },
|
||||||
) => {
|
) => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(() => [API_ENDPOINTS.TENDERS.DOCUMENTS(id)], [id]);
|
||||||
() => [API_ENDPOINTS.TENDERS.DOCUMENTS(id)],
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: () => tendersService.getDocuments(id),
|
queryFn: () => tendersService.getDocuments(id),
|
||||||
enabled: options?.enabled ?? true,
|
enabled: options?.enabled ?? true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { IMetaData } from "@/lib/api/types/Factory";
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
@@ -12,7 +11,14 @@ const isInvalidCursorError = (error: unknown) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseHybridPaginationOptions = {
|
type UseHybridPaginationOptions = {
|
||||||
meta?: IMetaData;
|
meta?: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
page?: number;
|
||||||
|
pages?: number;
|
||||||
|
has_more?: boolean;
|
||||||
|
next_cursor?: string;
|
||||||
|
};
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
setParams: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
setParams: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
@@ -91,7 +97,14 @@ export const useHybridPagination = ({
|
|||||||
|
|
||||||
if (meta?.page != null) return meta.page - 1;
|
if (meta?.page != null) return meta.page - 1;
|
||||||
return 0;
|
return 0;
|
||||||
}, [cursorStack, params.cursor, params.offset, meta?.offset, meta?.page, limit]);
|
}, [
|
||||||
|
cursorStack,
|
||||||
|
params.cursor,
|
||||||
|
params.offset,
|
||||||
|
meta?.offset,
|
||||||
|
meta?.page,
|
||||||
|
limit,
|
||||||
|
]);
|
||||||
|
|
||||||
const hasPrevious = cursorStack.length > 0 || currentPageIndex > 0;
|
const hasPrevious = cursorStack.length > 0 || currentPageIndex > 0;
|
||||||
|
|
||||||
@@ -129,7 +142,11 @@ export const useHybridPagination = ({
|
|||||||
{ cursor: nextCursor, fromPage: fromPage1Based },
|
{ cursor: nextCursor, fromPage: fromPage1Based },
|
||||||
]);
|
]);
|
||||||
setParams((current) => {
|
setParams((current) => {
|
||||||
const next: Record<string, any> = { ...current, cursor: nextCursor, limit };
|
const next: Record<string, any> = {
|
||||||
|
...current,
|
||||||
|
cursor: nextCursor,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
delete next.offset;
|
delete next.offset;
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { ApiResponse } from "../types";
|
|||||||
import {
|
import {
|
||||||
TTenderDetails,
|
TTenderDetails,
|
||||||
TTenderDocumentsResponse,
|
TTenderDocumentsResponse,
|
||||||
|
TTenderListParams,
|
||||||
|
TTenderListMeta,
|
||||||
TTenderResponse,
|
TTenderResponse,
|
||||||
TTenderTranslationResponse,
|
TTenderTranslationResponse,
|
||||||
} from "../types/Tenders";
|
} from "../types/Tenders";
|
||||||
|
|
||||||
export const tendersService = {
|
export const tendersService = {
|
||||||
tendersList: async (
|
tendersList: async (
|
||||||
params?: Record<string, any>,
|
params?: TTenderListParams,
|
||||||
): Promise<ApiResponse<TTenderResponse>> => {
|
): Promise<ApiResponse<TTenderResponse, TTenderListMeta>> => {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await api.get(API_ENDPOINTS.TENDERS.READ_ALL, {
|
await api.get(API_ENDPOINTS.TENDERS.READ_ALL, {
|
||||||
@@ -30,7 +32,8 @@ export const tendersService = {
|
|||||||
params?: Record<string, any>,
|
params?: Record<string, any>,
|
||||||
): Promise<ApiResponse<TTenderDetails>> => {
|
): Promise<ApiResponse<TTenderDetails>> => {
|
||||||
try {
|
try {
|
||||||
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id), { params })).data;
|
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id), { params }))
|
||||||
|
.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"ERROR caught in Tender Service => tender details: ",
|
"ERROR caught in Tender Service => tender details: ",
|
||||||
@@ -53,12 +56,17 @@ export const tendersService = {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("ERROR caught in Tender Service => translate tender: ", error);
|
console.error(
|
||||||
|
"ERROR caught in Tender Service => translate tender: ",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getDocuments: async (id: string): Promise<ApiResponse<TTenderDocumentsResponse[]>> => {
|
getDocuments: async (
|
||||||
|
id: string,
|
||||||
|
): Promise<ApiResponse<TTenderDocumentsResponse[]>> => {
|
||||||
try {
|
try {
|
||||||
return (await api.get(API_ENDPOINTS.TENDERS.DOCUMENTS(id))).data;
|
return (await api.get(API_ENDPOINTS.TENDERS.DOCUMENTS(id))).data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -123,9 +123,33 @@ export const TendersResponse = z.object({
|
|||||||
tenders: z.array(TenderSchema).or(z.null()),
|
tenders: z.array(TenderSchema).or(z.null()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TTenderDocumentsResponse = z.infer<typeof TenderDocumentsResponseSchema>;
|
export type TTenderDocumentsResponse = z.infer<
|
||||||
|
typeof TenderDocumentsResponseSchema
|
||||||
|
>;
|
||||||
export type TTenderResponse = z.infer<typeof TendersResponse>;
|
export type TTenderResponse = z.infer<typeof TendersResponse>;
|
||||||
export type TTenderDetails = z.infer<typeof TenderSchema>;
|
export type TTenderDetails = z.infer<typeof TenderSchema>;
|
||||||
|
export interface TTenderListParams {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
cursor?: string;
|
||||||
|
include_total?: boolean;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_order?: "asc" | "desc";
|
||||||
|
q?: string;
|
||||||
|
status?: string[];
|
||||||
|
country?: string;
|
||||||
|
documents_scraped?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface TTenderListMeta {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
has_more: boolean;
|
||||||
|
next_cursor?: string;
|
||||||
|
total?: number;
|
||||||
|
page?: number;
|
||||||
|
pages?: number;
|
||||||
|
}
|
||||||
export type TTenderTranslationResponse = z.infer<
|
export type TTenderTranslationResponse = z.infer<
|
||||||
typeof TenderTranslationResponseSchema
|
typeof TenderTranslationResponseSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IMetaData } from "./Factory";
|
import { IMetaData } from "./Factory";
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T, TMeta = IMetaData> {
|
||||||
data: T;
|
data: T;
|
||||||
message: string;
|
message: string;
|
||||||
code?: number;
|
code?: number;
|
||||||
meta?: IMetaData;
|
meta?: TMeta;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user