diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 103f173..2531dcb 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -1,24 +1,21 @@ -import { pgGetDashboardData, type DashboardTopItem, type DashboardSiteStats, type DashboardMonthStats } from "@/lib/pg-ff-client"; -import { TrendingUp, TrendingDown, Minus, Store, Euro, Percent, Users } from "lucide-react"; +import { pgGetDashboardData, type DashboardTopItem, type DashboardSiteStats, type DashboardSiteTop10 } from "@/lib/pg-ff-client"; +import { TrendingUp, TrendingDown, Minus, Store, Euro, Users } from "lucide-react"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -// Codes sites tels que retournés par l'API (ex: "F01") → nom affiché const SITE_NAMES: Record = { "F01": "Frouard", "F02": "Houdemont", - // Anciens codes PostgreSQL si jamais "292": "Frouard", "579": "Houdemont", }; function fmtEur(n: number): string { const abs = Math.abs(n); - if (abs >= 1_000_000) return `${(n / 1_000_000).toLocaleString("fr-FR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} M€`; - if (abs >= 1_000) return `${(n / 1_000).toLocaleString("fr-FR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} k€`; - return `${n.toLocaleString("fr-FR", { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €`; + 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")} €`; } function fmtQte(n: number): string { @@ -35,19 +32,13 @@ function delta(current: number, previous: number): number | null { return ((current - previous) / Math.abs(previous)) * 100; } -function monthLabel(mois: string): string { - const [y, m] = mois.split("-"); - const names = ["Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"]; - return `${names[parseInt(m) - 1]} ${y.slice(2)}`; -} - function dateLabel(iso: string): string { const [y, m, d] = iso.split("-"); return `${d}/${m}/${y}`; } // --------------------------------------------------------------------------- -// Sub-components (server-side, no "use client" needed) +// Sub-components // --------------------------------------------------------------------------- function DeltaBadge({ pct }: { pct: number | null }) { @@ -86,7 +77,6 @@ function KpiCard({ label, value, sub, delta: d, icon: Icon }: { function SiteCard({ site }: { site: DashboardSiteStats }) { const name = SITE_NAMES[site.site] ?? site.site; - const txMarge = site.ca_hier > 0 ? (site.marge_hier / site.ca_hier) * 100 : 0; const dCa = delta(site.ca_hier, site.ca_n1); const dTickets = delta(site.tickets_hier, site.tickets_n1); @@ -124,7 +114,7 @@ function Top10Table({ title, items, sortKey }: { }) { const maxVal = items[0]?.[sortKey] ?? 1; return ( -
+
{title}
@@ -157,7 +147,7 @@ function Top10Table({ title, items, sortKey }: {
- {sortKey !== "ca" && `CA ${fmtEur(item.ca)} • `} + {sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `} {fmtPct(txMarge, false)} marge
@@ -169,117 +159,19 @@ function Top10Table({ title, items, sortKey }: { ); } -function EvolutionTable({ evolution }: { evolution: DashboardMonthStats[] }) { - // Regrouper par mois YYYY-MM → data - const byMois = new Map(evolution.map(e => [e.mois, e])); - - // Calculer les 12 derniers mois complets (pas le mois en cours) - const now = new Date(); - const months12: string[] = []; - for (let i = 13; i >= 2; i--) { - const d = new Date(now.getFullYear(), now.getMonth() - i, 1); - months12.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`); - } - - const rows = months12.map(mois => { - const [y, m] = mois.split("-"); - const prevMois = `${parseInt(y) - 1}-${m}`; - const n = byMois.get(mois); - const n1 = byMois.get(prevMois); - const dCa = n && n1 ? delta(n.ca, n1.ca) : null; - const dQte = n && n1 ? delta(n.qte, n1.qte) : null; - return { mois, n, n1, dCa, dQte }; - }); - - // Trouver les maxCA pour les barres - const maxCa = Math.max(...rows.map(r => Math.max(r.n?.ca ?? 0, r.n1?.ca ?? 0)), 1); - +function SiteHitParade({ siteData, dateHier }: { siteData: DashboardSiteTop10; dateHier: string }) { + const name = SITE_NAMES[siteData.site] ?? siteData.site; return ( -
-
- Évolution mensuelle — CA réseau - N vs N-1 · 12 mois complets +
+
+ +

{name}

+ · {dateLabel(dateHier)}
-
- - - - - - - - - - - - - - - - {rows.map(({ mois, n, n1, dCa, dQte }) => { - const txN = n && n.ca > 0 ? (n.marge / n.ca) * 100 : null; - const barN = n ? (n.ca / maxCa) * 100 : 0; - const barN1 = n1 ? (n1.ca / maxCa) * 100 : 0; - const isPositive = dCa !== null && dCa >= 0; - return ( - - - - - - - - - - - - ); - })} - -
MoisCA NCA N-1Δ CAQté NQté N-1Δ QtéTx Marge NTendance
- {monthLabel(mois)} - - {n ? fmtEur(n.ca) : "—"} - - {n1 ? fmtEur(n1.ca) : "—"} - - {dCa !== null ? ( - - {fmtPct(dCa)} - - ) : "—"} - - {n ? fmtQte(n.qte) : "—"} - - {n1 ? fmtQte(n1.qte) : "—"} - - {dQte !== null ? ( - = 0 ? "#10b981" : "#ef4444" }}> - {fmtPct(dQte)} - - ) : "—"} - - {txN !== null ? ( - = 25 ? "#10b981" : txN >= 15 ? "#f59e0b" : "#ef4444" }}> - {fmtPct(txN, false)} - - ) : "—"} - -
-
- N -
-
-
-
-
- N-1 -
-
-
-
-
-
+
+ + +
); @@ -305,16 +197,15 @@ export default async function DashboardPage() { ); } - const { dateHier, sites, top10Ca, top10Qte, top10Marge, evolution } = data; + const { dateHier, dateN1, sites, top10BySite } = data; - // Totaux réseau const totalCa = sites.reduce((s, r) => s + r.ca_hier, 0); const totalCaN1 = sites.reduce((s, r) => s + r.ca_n1, 0); const totalTickets = sites.reduce((s, r) => s + r.tickets_hier, 0); const totalTicketsN1 = sites.reduce((s, r) => s + r.tickets_n1, 0); return ( -
+
{/* Header */}

@@ -322,7 +213,8 @@ export default async function DashboardPage() {

Données du {dateLabel(dateHier)} - {" "}· Comparaison vs même date N-1 + {" "}· Comparaison vs même jour de semaine N-1 + {dateN1 && ({dateLabel(dateN1)})}

@@ -364,24 +256,20 @@ export default async function DashboardPage() { )}
- {/* Hit Parade */} + {/* Hit Parade par magasin */}
-

- Hit Parade — réseau · {dateLabel(dateHier)} +

+ Hit Parade par magasin

-
- - - -
-
- - {/* Évolution N/N-1 */} -
-

- Évolution annuelle -

- + {top10BySite.length === 0 ? ( +

Aucune donnée pour hier.

+ ) : ( +
+ {top10BySite.map(s => ( + + ))} +
+ )}
); diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index b370a4a..6d334a4 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -7,7 +7,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
-
+
{children}
diff --git a/src/lib/pg-ff-client.ts b/src/lib/pg-ff-client.ts index 68948c0..dbb4615 100644 --- a/src/lib/pg-ff-client.ts +++ b/src/lib/pg-ff-client.ts @@ -485,20 +485,18 @@ export interface DashboardTopItem { marge: number; } -export interface DashboardMonthStats { - mois: string; // "YYYY-MM" - ca: number; - qte: number; - marge: number; +export interface DashboardSiteTop10 { + site: string; + ca: DashboardTopItem[]; + qte: DashboardTopItem[]; + marge: DashboardTopItem[]; } export interface DashboardData { dateHier: string; + dateN1: string; sites: DashboardSiteStats[]; - top10Ca: DashboardTopItem[]; - top10Qte: DashboardTopItem[]; - top10Marge: DashboardTopItem[]; - evolution: DashboardMonthStats[]; + top10BySite: DashboardSiteTop10[]; } // --------------------------------------------------------------------------- @@ -534,23 +532,27 @@ interface ApiPerfDashboard { /** * Retourne toutes les données nécessaires pour le dashboard quotidien. * 100% via l'API REST api.ffnancy.fr — aucune requête PostgreSQL directe. + * N-1 : même jour de semaine (52 semaines = 364 jours avant hier). */ export async function pgGetDashboardData(): Promise { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const dateHier = yesterday.toISOString().split("T")[0]; + // Même jour de semaine N-1 : 364 jours = 52 semaines exactes (conserve le jour) + const n1Date = new Date(yesterday); + n1Date.setDate(n1Date.getDate() - 364); + const dateN1Param = n1Date.toISOString().split("T")[0]; + const apiBase = "https://api.ffnancy.fr/api/performance"; - // 2 appels API en parallèle : dashboard (hier) + évolution mensuelle (si endpoint dispo) - const [apiResult, evoResult] = await Promise.all([ - fetch(`${apiBase}/dashboard?date=${dateHier}`, { cache: "no-store" }) - .then(r => { if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`); return r.json() as Promise; }), - // Évolution mensuelle — endpoint séparé (à confirmer), fallback à null - fetch(`${apiBase}/evolution`, { cache: "no-store" }) - .then(r => r.ok ? r.json() as Promise<{ months: DashboardMonthStats[] }> : null) - .catch(() => null), - ]); + const apiResult = await fetch( + `${apiBase}/dashboard?date=${dateHier}&date_n1=${dateN1Param}`, + { cache: "no-store" } + ).then(r => { + if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`); + return r.json() as Promise; + }); const sites: DashboardSiteStats[] = (apiResult.sites ?? []).map(s => ({ site: s.site, @@ -559,40 +561,47 @@ export async function pgGetDashboardData(): Promise { tickets_hier: Number(s.trafic) || 0, tickets_n1: Number(s.trafic_n1) || 0, qte_hier: 0, - marge_hier: 0, // non disponible dans l'API — affichera 0 + marge_hier: 0, lignes_hier: 0, })); - // Top 10 réseau : agréger les N lignes par site → top 10 global - const mapTop = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] => { - const byCodein = new Map(); - for (const i of items) { - const existing = byCodein.get(i.codein); - if (existing) { - existing.ca += Number(i.ca_ttc) || 0; - existing.qte += Number(i.qte_vendue) || 0; - existing.marge += Number(i.marge) || 0; - } else { - byCodein.set(i.codein, { - codein: i.codein, - libelle1: i.libelle1, - ca: Number(i.ca_ttc) || 0, - qte: Number(i.qte_vendue) || 0, - marge: Number(i.marge) || 0, - }); - } - } - return [...byCodein.values()].sort((a, b) => b[sortKey] - a[sortKey]).slice(0, 10); - }; + // Top 10 par magasin : grouper les items par site, trier, garder top 10 + const buildTop10 = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] => + items + .map(i => ({ + codein: i.codein, + libelle1: i.libelle1, + ca: Number(i.ca_ttc) || 0, + qte: Number(i.qte_vendue) || 0, + marge: Number(i.marge) || 0, + })) + .sort((a, b) => b[sortKey] - a[sortKey]) + .slice(0, 10); - console.log(`[pg-ff] Dashboard API: ${sites.length} sites, date=${apiResult.date}`); + const allItems = [ + ...(apiResult.top10_ca ?? []), + ...(apiResult.top10_qte ?? []), + ...(apiResult.top10_marge ?? []), + ]; + const siteSet = [...new Set(allItems.map(i => i.site).filter(Boolean))].sort(); + + const top10BySite: DashboardSiteTop10[] = siteSet.map(site => { + const byS = (arr: ApiPerfTopItem[]) => arr.filter(i => i.site === site); + return { + site, + ca: buildTop10(byS(apiResult.top10_ca ?? []), "ca"), + qte: buildTop10(byS(apiResult.top10_qte ?? []), "qte"), + marge: buildTop10(byS(apiResult.top10_marge ?? []), "marge"), + }; + }); + + const dateN1 = apiResult.date_n1 ?? dateN1Param; + console.log(`[pg-ff] Dashboard API: ${sites.length} sites, date=${apiResult.date}, date_n1=${dateN1}`); return { dateHier: apiResult.date ?? dateHier, + dateN1, sites, - top10Ca: mapTop(apiResult.top10_ca ?? [], "ca"), - top10Qte: mapTop(apiResult.top10_qte ?? [], "qte"), - top10Marge: mapTop(apiResult.top10_marge ?? [], "marge"), - evolution: evoResult?.months ?? [], + top10BySite, }; }