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:
AmirReza Jamali
2026-05-18 10:59:16 +03:30
parent 76b3a4afb3
commit a238edc563
18 changed files with 2293 additions and 65 deletions
@@ -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,
};
}