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,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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user