diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index fd8d1d3..36901b9 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -1,5 +1,5 @@ import { pgGetDashboardData, type DashboardTopItem, type DashboardSiteStats, type DashboardSiteTop10 } from "@/lib/pg-ff-client"; -import { TrendingUp, TrendingDown, Minus, Store, Euro, Users } from "lucide-react"; +import { TrendingUp, TrendingDown, Minus, Store, Euro, Users, ShoppingCart, Award, BarChart3 } from "lucide-react"; // --------------------------------------------------------------------------- // Helpers @@ -37,124 +37,449 @@ function dateLabel(iso: string): string { return `${d}/${m}/${y}`; } +// --------------------------------------------------------------------------- +// Design tokens (inline color helpers) +// --------------------------------------------------------------------------- + +const clrPositive = "#22c55e"; +const clrNegative = "#ef4444"; +const clrGold = "#f59e0b"; +const clrSilver = "#94a3b8"; +const clrBronze = "#cd7c2b"; + +const tableAccents = { + ca: { bar: "#3b82f6", header: "rgba(59,130,246,0.12)", border: "rgba(59,130,246,0.3)", label: "#60a5fa" }, + qte: { bar: "#8b5cf6", header: "rgba(139,92,246,0.12)", border: "rgba(139,92,246,0.3)", label: "#a78bfa" }, + marge: { bar: "#10b981", header: "rgba(16,185,129,0.12)", border: "rgba(16,185,129,0.3)", label: "#34d399" }, +}; + // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- -function DeltaBadge({ pct }: { pct: number | null }) { - if (pct === null) return —; +function DeltaBadge({ pct, large = false }: { pct: number | null; large?: boolean }) { + if (pct === null) return —; const isPos = pct >= 0; const Icon = pct > 0.5 ? TrendingUp : pct < -0.5 ? TrendingDown : Minus; + const color = isPos ? clrPositive : clrNegative; + const bg = isPos ? "rgba(34,197,94,0.1)" : "rgba(239,68,68,0.1)"; + const borderClr = isPos ? "rgba(34,197,94,0.25)" : "rgba(239,68,68,0.25)"; return ( - - + + {fmtPct(pct)} ); } -function KpiCard({ label, value, sub, delta: d, icon: Icon }: { - label: string; value: string; sub?: string; delta?: number | null; - icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; +function NetworkKpiCard({ + label, value, valueSub, delta: d, n1Value, n1Label, dateHier, dateN1, icon: Icon, accentColor, +}: { + label: string; + value: string; + valueSub?: string; + delta?: number | null; + n1Value: string; + n1Label: string; + dateHier: string; + dateN1: string; + icon: React.ComponentType<{ style?: React.CSSProperties }>; + accentColor: string; }) { return ( -
-
- {label} -
- +
+ {/* Decorative glow */} +
+ +
+
+ + {label} + +
+ + {value} + + {valueSub && ( + {valueSub} + )} +
+
+
+
- {value} -
- {d !== undefined && } - {sub && {sub}} + + {/* Delta badge */} + {d !== undefined && ( +
+ + + vs même jour N-1 + +
+ )} + + {/* Divider */} +
+ + {/* N-1 comparison row */} +
+
+ + Hier — {dateHier} + + + {value} + +
+
+
+ + N-1 — {dateN1} + + + {n1Value} + +
); } -function SiteCard({ site }: { site: DashboardSiteStats }) { +function StoreCard({ site, dateHier, dateN1 }: { site: DashboardSiteStats; dateHier: string; dateN1: string }) { const name = SITE_NAMES[site.site] ?? site.site; const dCa = delta(site.ca_hier, site.ca_n1); const dTickets = delta(site.tickets_hier, site.tickets_n1); + const isPos = dCa !== null && dCa >= 0; return ( -
-
- - {name} - site {site.site} -
-
-
-
CA
-
{fmtEur(site.ca_hier)}
-
- - {site.ca_n1 > 0 && N-1 {fmtEur(site.ca_n1)}} +
+ {/* Top accent bar */} +
+ +
+ {/* Store header */} +
+
+
+ +
+
+
+ {name} +
+
+ Site {site.site} +
+
+
+ +
+ + {/* Metrics row */} +
+ {/* CA */} +
+
+ + + Chiffre d'affaires + +
+
+ {fmtEur(site.ca_hier)} +
+
+ N-1 : + + {site.ca_n1 > 0 ? fmtEur(site.ca_n1) : "—"} + +
+
+ + {/* Tickets */} +
+
+ + + Trafic tickets + +
+
+ {fmtQte(site.tickets_hier)} +
+
+ + {site.tickets_n1 > 0 && ( + + N-1 : {fmtQte(site.tickets_n1)} + + )} +
-
-
Trafic (tickets)
-
{fmtQte(site.tickets_hier)}
-
- - {site.tickets_n1 > 0 && N-1 {fmtQte(site.tickets_n1)}} -
+ + {/* Date footer */} +
+ + Hier : {dateLabel(dateHier)} + + + N-1 : {dateLabel(dateN1)} +
); } -function Top10Table({ title, items, sortKey }: { - title: string; items: DashboardTopItem[]; sortKey: "ca" | "qte" | "marge"; -}) { - const maxVal = items[0]?.[sortKey] ?? 1; +function RankBadge({ rank }: { rank: number }) { + if (rank === 0) return ( + 1 + ); + if (rank === 1) return ( + 2 + ); + if (rank === 2) return ( + 3 + ); return ( -
-
- {title} + {rank + 1} + ); +} + +function Top10Table({ title, items, sortKey, accent }: { + title: string; + items: DashboardTopItem[]; + sortKey: "ca" | "qte" | "marge"; + accent: typeof tableAccents.ca; +}) { + const maxVal = Math.max(items[0]?.[sortKey] ?? 1, 1); + + return ( +
+ {/* Table header */} +
+
+ {sortKey === "ca" && } + {sortKey === "qte" && } + {sortKey === "marge" && } +
+ + {title} +
-
- {items.length === 0 && ( -
- Aucune donnée pour hier + + {/* Rows */} +
+ {items.length === 0 ? ( +
+ Aucune donnée
- )} - {items.map((item, i) => { - const val = item[sortKey]; - const barPct = maxVal > 0 ? (val / maxVal) * 100 : 0; - const txMarge = item.ca > 0 ? (item.marge / item.ca) * 100 : 0; - return ( -
-
- - {i + 1} - - - {item.libelle1 ?? item.codein} - - - {sortKey === "qte" ? fmtQte(val) : fmtEur(val)} - -
-
-
-
+ ) : ( + items.map((item, i) => { + const val = item[sortKey]; + const barPct = maxVal > 0 ? Math.max((val / maxVal) * 100, 2) : 0; + const txMarge = item.ca > 0 ? (item.marge / item.ca) * 100 : 0; + const isTopThree = i < 3; + + return ( +
+ {/* Main row */} +
+ + + {item.libelle1 ?? item.codein} + + + {sortKey === "qte" ? fmtQte(val) : fmtEur(val)} + +
+ + {/* Progress bar + meta */} +
+
+
+
+ + {sortKey === "ca" && `${fmtQte(item.qte)} pcs · `} + {sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `} + 25 ? "#22c55e" : txMarge > 15 ? "#f59e0b" : "var(--text-muted)" }}> + {fmtPct(txMarge, false)} mg + +
- - {sortKey === "ca" && `${fmtQte(item.qte)} pcs · `} - {sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `} - {fmtPct(txMarge, false)} marge -
-
- ); - })} + ); + }) + )}
); @@ -162,17 +487,54 @@ function Top10Table({ title, items, sortKey }: { function SiteHitParade({ siteData, dateHier }: { siteData: DashboardSiteTop10; dateHier: string }) { const name = SITE_NAMES[siteData.site] ?? siteData.site; + return ( -
-
- -

{name}

- · {dateLabel(dateHier)} +
+ {/* Section title */} +
+
+ +
+
+

+ Hit Parade — {name} +

+

+ Données du {dateLabel(dateHier)} · site {siteData.site} +

+
-
- - - + + {/* Three tables */} +
+ + + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Section divider +// --------------------------------------------------------------------------- + +function SectionHeader({ label, sub }: { label: string; sub?: string }) { + return ( +
+
+

+ {label} +

+ {sub &&

{sub}

}
); @@ -190,9 +552,28 @@ export default async function DashboardPage() { const msg = e instanceof Error ? e.message : String(e); console.error("[Dashboard] pgGetDashboardData failed:", e); return ( -
-

- Impossible de charger le tableau de bord : {msg.slice(0, 500)} +

+
+ +
+

+ Impossible de charger le tableau de bord +

+

+ {msg.slice(0, 300)}

); @@ -205,73 +586,131 @@ export default async function DashboardPage() { const totalTickets = sites.reduce((s, r) => s + r.tickets_hier, 0); const totalTicketsN1 = sites.reduce((s, r) => s + r.tickets_n1, 0); + const dCaReseau = delta(totalCa, totalCaN1); + const dTicketsReseau = delta(totalTickets, totalTicketsN1); + + const dHier = dateLabel(dateHier); + const dN1 = dateN1 ? dateLabel(dateN1) : "—"; + return ( -
- {/* Header */} -
-

- Tableau de Bord -

-

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

+
+ + {/* ── PAGE HEADER ─────────────────────────────────────────────── */} +
+ {/* subtle background pattern */} +
+ +
+

+ Tableau de Bord +

+

+ Données du{" "} + {dHier} + {" · "} + Comparaison vs même jour de semaine N-1{" "} + ({dN1}) +

+
+ +
+ + + {sites.length} magasin{sites.length > 1 ? "s" : ""} actif{sites.length > 1 ? "s" : ""} + +
- {/* KPI réseau */} -
- - - SITE_NAMES[s.site] ?? s.site).join(" · ")} - icon={TrendingUp} - /> + {/* ── KPI RÉSEAU ──────────────────────────────────────────────── */} +
+ + +
+ + +
- {/* Par magasin */} -
-

- Par magasin -

- {sites.length === 0 ? ( -

Aucune donnée pour hier.

- ) : ( -
- {sites.map(s => )} -
- )} -
+ {/* ── PAR MAGASIN ─────────────────────────────────────────────── */} + {sites.length > 0 && ( +
+ SITE_NAMES[s.site] ?? s.site).join(" · ")}`} /> - {/* Hit Parade par magasin */} -
-

- Hit Parade par magasin -

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

Aucune donnée pour hier.

- ) : ( -
- {top10BySite.map(s => ( - +
+ {sites.map(s => ( + ))}
- )} -
+
+ )} + + {/* ── HIT PARADE ──────────────────────────────────────────────── */} + {top10BySite.length > 0 && ( +
+ + +
+ {top10BySite.map((s, idx) => ( +
+ {idx > 0 && ( +
+ )} + +
+ ))} +
+
+ )} + + {/* ── EMPTY STATE ─────────────────────────────────────────────── */} + {sites.length === 0 && ( +
+ Aucune donnée disponible pour hier. +
+ )}
); }