feat(dashboard): refactor home page to utilize TenderDashboard component
- Replaced the previous home page structure with a simplified TenderDashboard component for improved clarity and maintainability. - Updated the .gitignore file to include IDE-specific files, ensuring a cleaner repository. - Enhanced the useTenderListPresenter to support new parsing logic for cpv_codes in search parameters. - Introduced a new utility function, buildQueryString, for better handling of query parameters in API requests.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import { 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 {
|
||||
data: recentData,
|
||||
isPending: recentPending,
|
||||
isError: recentError,
|
||||
refetch: refetchRecent,
|
||||
} = useTendersQuery({
|
||||
sort_by: "created_at",
|
||||
sort_order: "desc",
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
const realRecent = recentData?.data?.tenders ?? [];
|
||||
|
||||
const tenders = useMemo(
|
||||
() => (USE_FAKE_AGGREGATES ? buildFakeTenders(80) : []),
|
||||
[],
|
||||
);
|
||||
|
||||
const isPending = USE_FAKE_AGGREGATES ? false : recentPending;
|
||||
const isError = recentError;
|
||||
const refetch = refetchRecent;
|
||||
|
||||
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;
|
||||
|
||||
return {
|
||||
isPending,
|
||||
isError,
|
||||
refetch,
|
||||
tenders,
|
||||
recent,
|
||||
recentIsLoading,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user