feat(dashboard): integrate new dashboard queries and refactor closing soon component
- Replaced TTenderDetails with TDashboardClosingSoonItem in the ClosingSoonCard component for better type accuracy. - Removed the fake-data.ts file as it is no longer needed with the new dashboard queries. - Introduced new hooks for fetching dashboard data, including summary, trend, countries, notice types, and closing soon items. - Updated useDashboardData to utilize the new dashboard queries, enhancing data retrieval and management. - Enhanced the API service layer with new endpoints for dashboard functionalities, improving overall architecture.
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TTenderDetails } from "@/lib/api";
|
||||
import { TDashboardClosingSoonItem } from "@/lib/api";
|
||||
import gsap from "gsap";
|
||||
import Link from "next/link";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { ArrowRightIcon, ClockIcon, FireIcon } from "./icons";
|
||||
|
||||
type Props = {
|
||||
tenders: TTenderDetails[];
|
||||
tenders: TDashboardClosingSoonItem[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
@@ -121,12 +121,10 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
|
||||
day: "numeric",
|
||||
})
|
||||
: "No deadline"}
|
||||
{t.buyer_organization?.name && (
|
||||
{t.buyer_name && (
|
||||
<>
|
||||
<span className="opacity-50">•</span>
|
||||
<span className="truncate">
|
||||
{t.buyer_organization.name}
|
||||
</span>
|
||||
<span className="truncate">{t.buyer_name}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { TTenderDetails } from "@/lib/api";
|
||||
|
||||
const NOW = Math.floor(Date.now() / 1000);
|
||||
const DAY = 86_400;
|
||||
const HOUR = 3600;
|
||||
|
||||
const COUNTRIES = [
|
||||
"DE",
|
||||
"FR",
|
||||
"IT",
|
||||
"ES",
|
||||
"NL",
|
||||
"BE",
|
||||
"PT",
|
||||
"PL",
|
||||
"SE",
|
||||
"AT",
|
||||
"AE",
|
||||
"GB",
|
||||
];
|
||||
|
||||
const NOTICE_TYPES = [
|
||||
"CONTRACT_NOTICE",
|
||||
"PRIOR_INFORMATION",
|
||||
"CONTRACT_AWARD",
|
||||
"MODIFICATION",
|
||||
"CONCESSION",
|
||||
"DESIGN_CONTEST",
|
||||
];
|
||||
|
||||
const FORM_TYPES = ["PLANNING", "COMPETITION", "RESULT", "DIRECTIVE"];
|
||||
|
||||
const STATUSES = [
|
||||
"active",
|
||||
"active",
|
||||
"active",
|
||||
"active",
|
||||
"awarded",
|
||||
"awarded",
|
||||
"expired",
|
||||
"cancelled",
|
||||
"draft",
|
||||
];
|
||||
|
||||
const BUYERS = [
|
||||
"Ministerio de Hacienda",
|
||||
"Comune di Milano",
|
||||
"City of Rotterdam",
|
||||
"Bundesministerium für Verkehr",
|
||||
"Région Île-de-France",
|
||||
"Stockholm Stad",
|
||||
"Generalitat de Catalunya",
|
||||
"Wiener Stadtwerke",
|
||||
"Servicio Andaluz de Salud",
|
||||
"Dubai Municipality",
|
||||
"Camara Municipal de Lisboa",
|
||||
"Polska Grupa Energetyczna",
|
||||
"Ayuntamiento de Madrid",
|
||||
"Deutsche Bahn AG",
|
||||
"Aéroports de Paris",
|
||||
];
|
||||
|
||||
const TITLES = [
|
||||
"Construction of intermodal logistics hub",
|
||||
"Supply of energy-efficient LED street lighting",
|
||||
"Modernization of municipal water treatment plant",
|
||||
"IT consultancy for public sector cloud migration",
|
||||
"Renovation of historic city center facades",
|
||||
"Procurement of electric public transit buses",
|
||||
"Maintenance services for highway infrastructure",
|
||||
"Hospital medical equipment and consumables",
|
||||
"Smart city traffic monitoring system rollout",
|
||||
"Cybersecurity audit and penetration testing",
|
||||
"Educational software licenses for public schools",
|
||||
"Green roof installation across municipal buildings",
|
||||
"Catering services for government cafeterias",
|
||||
"Construction supervision of metro line extension",
|
||||
"Renewable energy plant feasibility study",
|
||||
"Digital archive platform for national library",
|
||||
"Waste collection and recycling vehicles supply",
|
||||
"Emergency response communications upgrade",
|
||||
"Cleaning and sanitation services framework",
|
||||
"Urban park redevelopment design contest",
|
||||
];
|
||||
|
||||
const CURRENCIES = ["EUR", "EUR", "EUR", "USD", "GBP", "AED"];
|
||||
|
||||
function rand(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function pick<T>(arr: T[]): T {
|
||||
return arr[rand(0, arr.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic seed so the dashboard looks stable across reloads
|
||||
* within the same calendar day (titles/timestamps shuffle once per day).
|
||||
*/
|
||||
function seededShuffle<T>(arr: T[], seed: number) {
|
||||
const out = [...arr];
|
||||
let s = seed;
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
s = (s * 9301 + 49297) % 233280;
|
||||
const j = Math.floor((s / 233280) * (i + 1));
|
||||
[out[i], out[j]] = [out[j], out[i]];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildFakeTenders(count = 80): TTenderDetails[] {
|
||||
const today = new Date();
|
||||
const seed =
|
||||
today.getFullYear() * 10000 +
|
||||
(today.getMonth() + 1) * 100 +
|
||||
today.getDate();
|
||||
|
||||
const titlePool = seededShuffle(TITLES, seed);
|
||||
const buyerPool = seededShuffle(BUYERS, seed + 1);
|
||||
|
||||
const tenders: TTenderDetails[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const country = COUNTRIES[i % COUNTRIES.length];
|
||||
const isUrgentDeadline = i < 8;
|
||||
const isFuture = i < 30;
|
||||
const status = isUrgentDeadline ? "active" : pick(STATUSES);
|
||||
|
||||
const created = NOW - rand(0, 14) * DAY - rand(0, 23) * HOUR;
|
||||
const published = created - rand(0, 2) * DAY;
|
||||
const submissionDeadline = isUrgentDeadline
|
||||
? NOW + rand(2, 6) * HOUR + i * DAY * 0.3
|
||||
: isFuture
|
||||
? NOW + rand(2, 21) * DAY
|
||||
: NOW - rand(1, 60) * DAY;
|
||||
const tenderDeadline = submissionDeadline + rand(0, 5) * DAY;
|
||||
|
||||
tenders.push({
|
||||
id: `fake-${i + 1}`,
|
||||
notice_publication_id: `PUB-${100000 + i}`,
|
||||
tender_id: `T-${2026000 + i}`,
|
||||
title: titlePool[i % titlePool.length],
|
||||
description:
|
||||
"Demonstration tender record generated for the dashboard preview. Replace with backend data when the dashboard API is wired up.",
|
||||
procurement_type_code: pick(["WORKS", "SUPPLIES", "SERVICES"]),
|
||||
procedure_code: pick(["OPEN", "RESTRICTED", "NEGOTIATED"]),
|
||||
main_classification: "45000000",
|
||||
estimated_value: rand(50_000, 12_000_000),
|
||||
currency: pick(CURRENCIES),
|
||||
publication_date: published,
|
||||
tender_deadline: tenderDeadline,
|
||||
submission_deadline: submissionDeadline,
|
||||
application_deadline: submissionDeadline,
|
||||
submission_url: "https://example.com/tender",
|
||||
country_code: country,
|
||||
buyer_organization: {
|
||||
name: buyerPool[i % buyerPool.length],
|
||||
contact_email: "procurement@example.com",
|
||||
},
|
||||
status,
|
||||
form_type: pick(FORM_TYPES),
|
||||
notice_type_code: pick(NOTICE_TYPES),
|
||||
created_at: created,
|
||||
} as unknown as TTenderDetails);
|
||||
}
|
||||
|
||||
return tenders;
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useTendersQuery } from "@/hooks/queries";
|
||||
import {
|
||||
useDashboardClosingSoonQuery,
|
||||
useDashboardCountriesQuery,
|
||||
useDashboardNoticeTypesQuery,
|
||||
useDashboardSummaryQuery,
|
||||
useDashboardTrendQuery,
|
||||
useTendersQuery,
|
||||
} from "@/hooks/queries";
|
||||
import { useMemo } from "react";
|
||||
import { buildFakeTenders } from "./fake-data";
|
||||
|
||||
const NOW_SECONDS = () => Math.floor(Date.now() / 1000);
|
||||
const DAY = 86_400;
|
||||
|
||||
/**
|
||||
* Toggle to `false` once the aggregate dashboard API documented in
|
||||
* `docs/backend/dashboard-api.md` is available — the hook will then
|
||||
* source summary/trend/country metrics from the backend instead of
|
||||
* the bundled demo set. The "Recent Tenders" list always uses the
|
||||
* real `useTendersQuery` source.
|
||||
*/
|
||||
const USE_FAKE_AGGREGATES = true;
|
||||
|
||||
export type DashboardMetrics = ReturnType<typeof useDashboardData>;
|
||||
|
||||
export function useDashboardData() {
|
||||
const summaryQuery = useDashboardSummaryQuery();
|
||||
const trendQuery = useDashboardTrendQuery({ days: 14, metric: "created" });
|
||||
const countriesQuery = useDashboardCountriesQuery({ limit: 6 });
|
||||
const noticeTypesQuery = useDashboardNoticeTypesQuery();
|
||||
const closingSoonQuery = useDashboardClosingSoonQuery({ limit: 5 });
|
||||
|
||||
const {
|
||||
data: recentData,
|
||||
isPending: recentPending,
|
||||
@@ -31,136 +31,88 @@ export function useDashboardData() {
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
const realRecent = recentData?.data?.tenders ?? [];
|
||||
const summary = summaryQuery.data?.data;
|
||||
const trendResponse = trendQuery.data?.data;
|
||||
const countriesResponse = countriesQuery.data?.data;
|
||||
const noticeTypesResponse = noticeTypesQuery.data?.data;
|
||||
const closingSoonList = closingSoonQuery.data?.data?.items ?? [];
|
||||
|
||||
const tenders = useMemo(
|
||||
() => (USE_FAKE_AGGREGATES ? buildFakeTenders(80) : []),
|
||||
[],
|
||||
const isPending =
|
||||
summaryQuery.isPending ||
|
||||
trendQuery.isPending ||
|
||||
countriesQuery.isPending ||
|
||||
noticeTypesQuery.isPending ||
|
||||
closingSoonQuery.isPending;
|
||||
|
||||
const isError =
|
||||
summaryQuery.isError ||
|
||||
trendQuery.isError ||
|
||||
countriesQuery.isError ||
|
||||
noticeTypesQuery.isError ||
|
||||
closingSoonQuery.isError ||
|
||||
recentError;
|
||||
|
||||
const refetch = () => {
|
||||
summaryQuery.refetch();
|
||||
trendQuery.refetch();
|
||||
countriesQuery.refetch();
|
||||
noticeTypesQuery.refetch();
|
||||
closingSoonQuery.refetch();
|
||||
refetchRecent();
|
||||
};
|
||||
|
||||
const trend = useMemo(() => {
|
||||
const series = trendResponse?.series ?? [];
|
||||
return series.map((point) => {
|
||||
const [y, m, d] = point.date.split("-").map(Number);
|
||||
const date = new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1));
|
||||
return {
|
||||
date: point.date,
|
||||
label: date.toLocaleDateString("en", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}),
|
||||
count: point.count,
|
||||
};
|
||||
});
|
||||
}, [trendResponse]);
|
||||
|
||||
const countries = useMemo(
|
||||
() =>
|
||||
(countriesResponse?.items ?? []).map((item) => ({
|
||||
code: item.country_code,
|
||||
count: item.count,
|
||||
})),
|
||||
[countriesResponse],
|
||||
);
|
||||
|
||||
const isPending = USE_FAKE_AGGREGATES ? false : recentPending;
|
||||
const isError = recentError;
|
||||
const refetch = refetchRecent;
|
||||
const noticeTypes = useMemo(
|
||||
() =>
|
||||
(noticeTypesResponse?.items ?? []).map((item) => ({
|
||||
type: item.type,
|
||||
count: item.count,
|
||||
})),
|
||||
[noticeTypesResponse],
|
||||
);
|
||||
|
||||
const derived = useMemo(() => {
|
||||
const now = NOW_SECONDS();
|
||||
const total = tenders.length;
|
||||
|
||||
let active = 0;
|
||||
let closingSoon = 0;
|
||||
let awarded = 0;
|
||||
let totalValue = 0;
|
||||
let valueCurrency = "EUR";
|
||||
|
||||
const countryCounts = new Map<string, number>();
|
||||
const noticeTypeCounts = new Map<string, number>();
|
||||
const dailyCounts = new Map<string, number>();
|
||||
|
||||
for (const t of tenders) {
|
||||
const status = (t.status || "").toLowerCase();
|
||||
if (status === "active") active += 1;
|
||||
if (status === "awarded") awarded += 1;
|
||||
|
||||
const deadline = t.submission_deadline || t.tender_deadline || 0;
|
||||
if (deadline > now && deadline - now < 7 * DAY) closingSoon += 1;
|
||||
|
||||
if (typeof t.estimated_value === "number" && t.estimated_value > 0) {
|
||||
totalValue += t.estimated_value;
|
||||
if (t.currency) valueCurrency = t.currency;
|
||||
}
|
||||
|
||||
if (t.country_code) {
|
||||
countryCounts.set(
|
||||
t.country_code,
|
||||
(countryCounts.get(t.country_code) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
|
||||
const noticeKey =
|
||||
t.form_type || t.notice_type_code || t.procurement_type_code || "OTHER";
|
||||
noticeTypeCounts.set(
|
||||
noticeKey,
|
||||
(noticeTypeCounts.get(noticeKey) ?? 0) + 1,
|
||||
);
|
||||
|
||||
const created = t.created_at || t.publication_date;
|
||||
if (created) {
|
||||
const d = new Date(created * 1000);
|
||||
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
dailyCounts.set(key, (dailyCounts.get(key) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const countries = Array.from(countryCounts.entries())
|
||||
.map(([code, count]) => ({ code, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
const noticeTypes = Array.from(noticeTypeCounts.entries())
|
||||
.map(([type, count]) => ({ type, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 6);
|
||||
|
||||
const trendDays = 14;
|
||||
const trend: { date: string; label: string; count: number }[] = [];
|
||||
for (let i = trendDays - 1; i >= 0; i--) {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() - i);
|
||||
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
trend.push({
|
||||
date: key,
|
||||
label: d.toLocaleDateString("en", { month: "short", day: "numeric" }),
|
||||
count: dailyCounts.get(key) ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
const fakeRecent = [...tenders]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.created_at || b.publication_date || 0) -
|
||||
(a.created_at || a.publication_date || 0),
|
||||
)
|
||||
.slice(0, 5);
|
||||
|
||||
const closingSoonList = tenders
|
||||
.filter((t) => {
|
||||
const dl = t.submission_deadline || t.tender_deadline || 0;
|
||||
return dl > now;
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.submission_deadline || a.tender_deadline || 0) -
|
||||
(b.submission_deadline || b.tender_deadline || 0),
|
||||
)
|
||||
.slice(0, 5);
|
||||
|
||||
return {
|
||||
total,
|
||||
active,
|
||||
awarded,
|
||||
closingSoon,
|
||||
totalValue,
|
||||
valueCurrency,
|
||||
countries,
|
||||
noticeTypes,
|
||||
trend,
|
||||
fakeRecent,
|
||||
closingSoonList,
|
||||
};
|
||||
}, [tenders]);
|
||||
|
||||
const recent = realRecent.length ? realRecent : derived.fakeRecent;
|
||||
const recentIsLoading = recentPending && realRecent.length === 0;
|
||||
|
||||
const { fakeRecent: _ignored, ...rest } = derived;
|
||||
const recent = recentData?.data?.tenders ?? [];
|
||||
const recentIsLoading = recentPending && recent.length === 0;
|
||||
|
||||
return {
|
||||
isPending,
|
||||
isError,
|
||||
refetch,
|
||||
tenders,
|
||||
total: summary?.total_tenders ?? 0,
|
||||
active: summary?.active_tenders ?? 0,
|
||||
awarded: summary?.awarded_tenders ?? 0,
|
||||
closingSoon: summary?.closing_soon ?? 0,
|
||||
totalValue: summary?.total_estimated_value ?? 0,
|
||||
valueCurrency: summary?.value_currency ?? "EUR",
|
||||
countries,
|
||||
noticeTypes,
|
||||
trend,
|
||||
recent,
|
||||
recentIsLoading,
|
||||
...rest,
|
||||
closingSoonList,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TenderDetailsError } from "./components/tender-details-error";
|
||||
import { TenderDetailsHeader } from "./components/tender-details-header";
|
||||
import { useTenderSectionScrollSpy } from "./hooks/use-tender-section-scroll-spy";
|
||||
import { getTenderNavItems } from "./tender-nav-utils";
|
||||
import { shouldShowAiSummarySection } from "./tender-display-utils";
|
||||
|
||||
interface IProps {
|
||||
params: Promise<{
|
||||
@@ -43,11 +44,14 @@ const TenderDetails = ({ params }: IProps) => {
|
||||
];
|
||||
|
||||
const tenderForNav = data?.data;
|
||||
const hasValidAiSummary = tenderForNav
|
||||
? shouldShowAiSummarySection(tenderForNav)
|
||||
: false;
|
||||
const { data: documentsResponse, isPending: isDocumentsLoading } =
|
||||
useGetTenderDocumentsQuery(details);
|
||||
useGetTenderDocumentsQuery(details, { enabled: hasValidAiSummary });
|
||||
const documents = documentsResponse?.data ?? [];
|
||||
const showDocumentsSection =
|
||||
isDocumentsLoading || documents.length > 0;
|
||||
hasValidAiSummary && (isDocumentsLoading || documents.length > 0);
|
||||
|
||||
const tenderNavItems = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -335,7 +335,7 @@ export function Select<T extends FieldValues>({
|
||||
id={listboxId}
|
||||
role="listbox"
|
||||
aria-label={label}
|
||||
className="z-[200] max-h-[min(22rem,calc(100vh-6rem))] overflow-hidden rounded-xl border border-stroke/70 bg-white shadow-[0_18px_50px_-12px_rgba(15,23,42,0.25)] dark:border-white/10 dark:bg-dark-2 dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.65)]"
|
||||
className="z-[1000000] max-h-[min(22rem,calc(100vh-6rem))] overflow-hidden rounded-xl border border-stroke/70 bg-white shadow-[0_18px_50px_-12px_rgba(15,23,42,0.25)] dark:border-white/10 dark:bg-dark-2 dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.65)]"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: panelRect.top,
|
||||
|
||||
@@ -15,3 +15,4 @@ export * from "./useTenders";
|
||||
export * from "./useUsersQueries";
|
||||
export * from "./useAdminListQuery";
|
||||
export * from "./useSelectSearchQuery";
|
||||
export * from "./useDashboardQueries";
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { API_ENDPOINTS, dashboardService } from "@/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useDashboardSummaryQuery = (params?: {
|
||||
closing_window?: number;
|
||||
}) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.DASHBOARD.SUMMARY, params],
|
||||
[params],
|
||||
);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => dashboardService.summary(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDashboardTrendQuery = (params?: {
|
||||
days?: number;
|
||||
metric?: "created" | "published" | "awarded";
|
||||
}) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.DASHBOARD.TREND, params],
|
||||
[params],
|
||||
);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => dashboardService.trend(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDashboardCountriesQuery = (params?: { limit?: number }) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.DASHBOARD.COUNTRIES, params],
|
||||
[params],
|
||||
);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => dashboardService.countries(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDashboardNoticeTypesQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: [API_ENDPOINTS.DASHBOARD.NOTICE_TYPES],
|
||||
queryFn: () => dashboardService.noticeTypes(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDashboardClosingSoonQuery = (params?: {
|
||||
limit?: number;
|
||||
window?: number;
|
||||
}) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.DASHBOARD.CLOSING_SOON, params],
|
||||
[params],
|
||||
);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => dashboardService.closingSoon(params),
|
||||
});
|
||||
};
|
||||
@@ -48,7 +48,10 @@ export const useTranslateTenderMutation = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useGetTenderDocumentsQuery = (id: string) => {
|
||||
export const useGetTenderDocumentsQuery = (
|
||||
id: string,
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.TENDERS.DOCUMENTS(id)],
|
||||
[id],
|
||||
@@ -56,5 +59,6 @@ export const useGetTenderDocumentsQuery = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => tendersService.getDocuments(id),
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "/api/proxy/",
|
||||
timeout: 10000,
|
||||
timeout: 60000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -76,4 +76,13 @@ export const API_ENDPOINTS = {
|
||||
BASE: (id?: string) => (id ? `inquiries/${id}` : "inquiries"),
|
||||
STATUS: (id: string) => `inquiries/${id}/status`,
|
||||
},
|
||||
|
||||
DASHBOARD: {
|
||||
SUMMARY: "dashboard/summary",
|
||||
TREND: "dashboard/trend",
|
||||
COUNTRIES: "dashboard/countries",
|
||||
NOTICE_TYPES: "dashboard/notice-types",
|
||||
CLOSING_SOON: "dashboard/closing-soon",
|
||||
RECENT: "dashboard/recent",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import {
|
||||
ApiResponse,
|
||||
TDashboardClosingSoon,
|
||||
TDashboardCountries,
|
||||
TDashboardNoticeTypes,
|
||||
TDashboardSummary,
|
||||
TDashboardTrend,
|
||||
} from "../types";
|
||||
|
||||
export const dashboardService = {
|
||||
summary: async (params?: {
|
||||
closing_window?: number;
|
||||
}): Promise<ApiResponse<TDashboardSummary>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.DASHBOARD.SUMMARY, { params })).data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in Dashboard Service => summary:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
trend: async (params?: {
|
||||
days?: number;
|
||||
metric?: "created" | "published" | "awarded";
|
||||
}): Promise<ApiResponse<TDashboardTrend>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.DASHBOARD.TREND, { params })).data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in Dashboard Service => trend:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
countries: async (params?: {
|
||||
limit?: number;
|
||||
}): Promise<ApiResponse<TDashboardCountries>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.DASHBOARD.COUNTRIES, { params })).data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in Dashboard Service => countries:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
noticeTypes: async (): Promise<ApiResponse<TDashboardNoticeTypes>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.DASHBOARD.NOTICE_TYPES)).data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR caught in Dashboard Service => notice-types:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
closingSoon: async (params?: {
|
||||
limit?: number;
|
||||
window?: number;
|
||||
}): Promise<ApiResponse<TDashboardClosingSoon>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.DASHBOARD.CLOSING_SOON, { params }))
|
||||
.data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR caught in Dashboard Service => closing-soon:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./companies-service";
|
||||
export * from "./dashboard-service";
|
||||
export * from "./inquiries-service";
|
||||
export * from "./profile-service";
|
||||
export * from "./tenders-service";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
export type TDashboardSummary = {
|
||||
total_tenders: number;
|
||||
active_tenders: number;
|
||||
awarded_tenders: number;
|
||||
expired_tenders: number;
|
||||
cancelled_tenders: number;
|
||||
closing_soon: number;
|
||||
total_estimated_value: number;
|
||||
value_currency: string;
|
||||
generated_at: number;
|
||||
};
|
||||
|
||||
export type TDashboardTrendPoint = {
|
||||
date: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type TDashboardTrend = {
|
||||
metric: "created" | "published" | "awarded";
|
||||
days: number;
|
||||
series: TDashboardTrendPoint[];
|
||||
};
|
||||
|
||||
export type TDashboardCountryItem = {
|
||||
country_code: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type TDashboardCountries = {
|
||||
total: number;
|
||||
items: TDashboardCountryItem[];
|
||||
other_count: number;
|
||||
};
|
||||
|
||||
export type TDashboardNoticeTypeItem = {
|
||||
type: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type TDashboardNoticeTypes = {
|
||||
total: number;
|
||||
items: TDashboardNoticeTypeItem[];
|
||||
};
|
||||
|
||||
export type TDashboardClosingSoonItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
country_code: string;
|
||||
buyer_name?: string;
|
||||
submission_deadline: number;
|
||||
tender_deadline?: number;
|
||||
status: string;
|
||||
estimated_value?: number;
|
||||
currency?: string;
|
||||
};
|
||||
|
||||
export type TDashboardClosingSoon = {
|
||||
items: TDashboardClosingSoonItem[];
|
||||
};
|
||||
@@ -10,3 +10,4 @@ export * from "./TInquiries";
|
||||
export * from "./TCms";
|
||||
export * from "./Feedback";
|
||||
export * from "./Tenders";
|
||||
export * from "./Dashboard";
|
||||
|
||||
Reference in New Issue
Block a user