mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-20 05:28:11 +02:00
feat: Introduce direct PostgreSQL client for FF Nancy data to replace HTTP API calls and add a new dashboard page.
This commit is contained in:
1 parent
d1bbe290da
commit
6ee0e3294d
2 files changed
+32
-22
No files matched your search
@@ -15,7 +15,7 @@ const SITE_NAMES: Record<string, string> = {
|
|||||||
function fmtEur(n: number): string {
|
function fmtEur(n: number): string {
|
||||||
const abs = Math.abs(n);
|
const abs = Math.abs(n);
|
||||||
if (abs >= 1_000_000) return `${(n / 1_000_000).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} M€`;
|
if (abs >= 1_000_000) return `${(n / 1_000_000).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} M€`;
|
||||||
return `${Math.round(n).toLocaleString("fr-FR")} €`;
|
return `${n.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtQte(n: number): string {
|
function fmtQte(n: number): string {
|
||||||
@@ -147,6 +147,7 @@ function Top10Table({ title, items, sortKey }: {
|
|||||||
<div className="h-full rounded-full" style={{ width: `${barPct}%`, background: "var(--accent)" }} />
|
<div className="h-full rounded-full" style={{ width: `${barPct}%`, background: "var(--accent)" }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs shrink-0" style={{ color: "var(--text-muted)" }}>
|
<span className="text-xs shrink-0" style={{ color: "var(--text-muted)" }}>
|
||||||
|
{sortKey === "ca" && `${fmtQte(item.qte)} pcs · `}
|
||||||
{sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `}
|
{sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `}
|
||||||
{fmtPct(txMarge, false)} marge
|
{fmtPct(txMarge, false)} marge
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+30
-21
@@ -539,31 +539,41 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
const dateHier = yesterday.toISOString().split("T")[0];
|
const dateHier = yesterday.toISOString().split("T")[0];
|
||||||
|
|
||||||
// Même jour de semaine N-1 : 364 jours = 52 semaines exactes (conserve le jour)
|
// N-1 : même jour de semaine, même semaine N-1 → 364 jours = 52 semaines exactes
|
||||||
const n1Date = new Date(yesterday);
|
const n1Date = new Date(yesterday);
|
||||||
n1Date.setDate(n1Date.getDate() - 364);
|
n1Date.setDate(n1Date.getDate() - 364);
|
||||||
const dateN1Param = n1Date.toISOString().split("T")[0];
|
const dateN1Param = n1Date.toISOString().split("T")[0];
|
||||||
|
|
||||||
const apiBase = "https://api.ffnancy.fr/api/performance";
|
const apiBase = "https://api.ffnancy.fr/api/performance";
|
||||||
|
|
||||||
const apiResult = await fetch(
|
// Deux appels parallèles : données hier + données même jour N-1
|
||||||
`${apiBase}/dashboard?date=${dateHier}&date_n1=${dateN1Param}`,
|
const [apiResult, apiN1Result] = await Promise.all([
|
||||||
{ cache: "no-store" }
|
fetch(`${apiBase}/dashboard?date=${dateHier}`, { cache: "no-store" })
|
||||||
).then(r => {
|
.then(r => { if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`); return r.json() as Promise<ApiPerfDashboard>; }),
|
||||||
if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`);
|
fetch(`${apiBase}/dashboard?date=${dateN1Param}`, { cache: "no-store" })
|
||||||
return r.json() as Promise<ApiPerfDashboard>;
|
.then(r => r.ok ? r.json() as Promise<ApiPerfDashboard> : null)
|
||||||
});
|
.catch(() => null),
|
||||||
|
]);
|
||||||
|
|
||||||
const sites: DashboardSiteStats[] = (apiResult.sites ?? []).map(s => ({
|
// Construire map N-1 par site depuis l'appel séparé
|
||||||
site: s.site,
|
const n1BySite = new Map<string, { ca: number; trafic: number }>();
|
||||||
ca_hier: Number(s.ca_ttc) || 0,
|
for (const s of apiN1Result?.sites ?? []) {
|
||||||
ca_n1: Number(s.ca_ttc_n1) || 0,
|
n1BySite.set(s.site, { ca: Number(s.ca_ttc) || 0, trafic: Number(s.trafic) || 0 });
|
||||||
tickets_hier: Number(s.trafic) || 0,
|
}
|
||||||
tickets_n1: Number(s.trafic_n1) || 0,
|
|
||||||
qte_hier: 0,
|
const sites: DashboardSiteStats[] = (apiResult.sites ?? []).map(s => {
|
||||||
marge_hier: 0,
|
const n1 = n1BySite.get(s.site);
|
||||||
lignes_hier: 0,
|
return {
|
||||||
}));
|
site: s.site,
|
||||||
|
ca_hier: Number(s.ca_ttc) || 0,
|
||||||
|
ca_n1: n1?.ca ?? Number(s.ca_ttc_n1) || 0,
|
||||||
|
tickets_hier: Number(s.trafic) || 0,
|
||||||
|
tickets_n1: n1?.trafic ?? Number(s.trafic_n1) || 0,
|
||||||
|
qte_hier: 0,
|
||||||
|
marge_hier: 0,
|
||||||
|
lignes_hier: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Top 10 par magasin : grouper les items par site, trier, garder top 10
|
// Top 10 par magasin : grouper les items par site, trier, garder top 10
|
||||||
const buildTop10 = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] =>
|
const buildTop10 = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] =>
|
||||||
@@ -595,12 +605,11 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const dateN1 = apiResult.date_n1 ?? dateN1Param;
|
console.log(`[pg-ff] Dashboard: hier=${dateHier} n1=${dateN1Param} sites=${sites.length} n1_found=${n1BySite.size}`);
|
||||||
console.log(`[pg-ff] Dashboard API: ${sites.length} sites, date=${apiResult.date}, date_n1=${dateN1}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dateHier: apiResult.date ?? dateHier,
|
dateHier: apiResult.date ?? dateHier,
|
||||||
dateN1,
|
dateN1: apiN1Result?.date ?? dateN1Param,
|
||||||
sites,
|
sites,
|
||||||
top10BySite,
|
top10BySite,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user