From f080d51e637efd7d7cb2857cc6a7c18694d026e5 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Wed, 20 May 2026 13:01:36 +0330 Subject: [PATCH] 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. --- .../_components/dashboard/closing-soon.tsx | 10 +- .../(home)/_components/dashboard/fake-data.ts | 168 ------------- .../dashboard/use-dashboard-data.ts | 220 +++++++----------- src/app/tenders/[details]/page.tsx | 8 +- src/components/FormElements/select.tsx | 2 +- src/hooks/queries/index.ts | 1 + src/hooks/queries/useDashboardQueries.ts | 62 +++++ src/hooks/queries/useTenders.ts | 6 +- src/lib/api/axios.ts | 2 +- src/lib/api/endpoints.ts | 9 + src/lib/api/services/dashboard-service.ts | 74 ++++++ src/lib/api/services/index.ts | 1 + src/lib/api/types/Dashboard.ts | 59 +++++ src/lib/api/types/index.ts | 1 + 14 files changed, 310 insertions(+), 313 deletions(-) delete mode 100644 src/app/(home)/_components/dashboard/fake-data.ts create mode 100644 src/hooks/queries/useDashboardQueries.ts create mode 100644 src/lib/api/services/dashboard-service.ts create mode 100644 src/lib/api/types/Dashboard.ts diff --git a/src/app/(home)/_components/dashboard/closing-soon.tsx b/src/app/(home)/_components/dashboard/closing-soon.tsx index c695c73..d0adbcb 100644 --- a/src/app/(home)/_components/dashboard/closing-soon.tsx +++ b/src/app/(home)/_components/dashboard/closing-soon.tsx @@ -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 && ( <> - - {t.buyer_organization.name} - + {t.buyer_name} )}

diff --git a/src/app/(home)/_components/dashboard/fake-data.ts b/src/app/(home)/_components/dashboard/fake-data.ts deleted file mode 100644 index 648d2a5..0000000 --- a/src/app/(home)/_components/dashboard/fake-data.ts +++ /dev/null @@ -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(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(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; -} diff --git a/src/app/(home)/_components/dashboard/use-dashboard-data.ts b/src/app/(home)/_components/dashboard/use-dashboard-data.ts index 5cf7eb8..9ad5569 100644 --- a/src/app/(home)/_components/dashboard/use-dashboard-data.ts +++ b/src/app/(home)/_components/dashboard/use-dashboard-data.ts @@ -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; 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(); - const noticeTypeCounts = new Map(); - const dailyCounts = new Map(); - - 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, }; } diff --git a/src/app/tenders/[details]/page.tsx b/src/app/tenders/[details]/page.tsx index a41ba50..7af2307 100644 --- a/src/app/tenders/[details]/page.tsx +++ b/src/app/tenders/[details]/page.tsx @@ -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( () => diff --git a/src/components/FormElements/select.tsx b/src/components/FormElements/select.tsx index 19d1ea6..a5da356 100644 --- a/src/components/FormElements/select.tsx +++ b/src/components/FormElements/select.tsx @@ -335,7 +335,7 @@ export function Select({ 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, diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 2993eef..ab221cc 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -15,3 +15,4 @@ export * from "./useTenders"; export * from "./useUsersQueries"; export * from "./useAdminListQuery"; export * from "./useSelectSearchQuery"; +export * from "./useDashboardQueries"; diff --git a/src/hooks/queries/useDashboardQueries.ts b/src/hooks/queries/useDashboardQueries.ts new file mode 100644 index 0000000..0dca227 --- /dev/null +++ b/src/hooks/queries/useDashboardQueries.ts @@ -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), + }); +}; diff --git a/src/hooks/queries/useTenders.ts b/src/hooks/queries/useTenders.ts index 7d3de6b..d390427 100644 --- a/src/hooks/queries/useTenders.ts +++ b/src/hooks/queries/useTenders.ts @@ -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, }); }; \ No newline at end of file diff --git a/src/lib/api/axios.ts b/src/lib/api/axios.ts index 23c6d8e..03c8642 100644 --- a/src/lib/api/axios.ts +++ b/src/lib/api/axios.ts @@ -10,7 +10,7 @@ import axios from "axios"; const api = axios.create({ baseURL: "/api/proxy/", - timeout: 10000, + timeout: 60000, headers: { "Content-Type": "application/json", }, diff --git a/src/lib/api/endpoints.ts b/src/lib/api/endpoints.ts index 4f9999c..61fe7ac 100644 --- a/src/lib/api/endpoints.ts +++ b/src/lib/api/endpoints.ts @@ -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; diff --git a/src/lib/api/services/dashboard-service.ts b/src/lib/api/services/dashboard-service.ts new file mode 100644 index 0000000..a4def45 --- /dev/null +++ b/src/lib/api/services/dashboard-service.ts @@ -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> => { + 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> => { + 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> => { + 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> => { + 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> => { + 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; + } + }, +}; diff --git a/src/lib/api/services/index.ts b/src/lib/api/services/index.ts index 5149056..23d11a4 100644 --- a/src/lib/api/services/index.ts +++ b/src/lib/api/services/index.ts @@ -1,4 +1,5 @@ export * from "./companies-service"; +export * from "./dashboard-service"; export * from "./inquiries-service"; export * from "./profile-service"; export * from "./tenders-service"; diff --git a/src/lib/api/types/Dashboard.ts b/src/lib/api/types/Dashboard.ts new file mode 100644 index 0000000..f239fd5 --- /dev/null +++ b/src/lib/api/types/Dashboard.ts @@ -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[]; +}; diff --git a/src/lib/api/types/index.ts b/src/lib/api/types/index.ts index 6a0ab72..6d02388 100644 --- a/src/lib/api/types/index.ts +++ b/src/lib/api/types/index.ts @@ -10,3 +10,4 @@ export * from "./TInquiries"; export * from "./TCms"; export * from "./Feedback"; export * from "./Tenders"; +export * from "./Dashboard";