import type { Metadata } from "next";
import Link from "next/link";
import { unstable_cache } from "next/cache";
import { BarChart3, Euro, House, Receipt, ShoppingBasket, ShoppingCart, Store } from "lucide-react";
import { pgGetDashboardData, type DashboardSiteStats, type DashboardSiteTop10, type DashboardTopItem } from "@/lib/pg-ff-client";
import { PageHeader } from "@/components/ui/page-header";
import { StatCard } from "@/components/ui/stat-card";
import { Card, CardHeader } from "@/components/ui/card";
import { Badge, DeltaBadge } from "@/components/ui/badge";
import { EmptyState, ErrorState } from "@/components/ui/states";
import { Terme } from "@/components/ui/tooltip";
import { LIBELLE_TOUS_MAGASINS, nomMagasin } from "@/lib/magasins";
import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format";
import { couleurMarge } from "@/lib/marge";
export const metadata: Metadata = { title: "Accueil" };
const getCachedDashboardData = unstable_cache(
pgGetDashboardData,
["dashboard-data"],
// 30 minutes : ce sont les ventes de la veille, elles ne changent pas dans la journée.
{ revalidate: 1800 }
);
// ---------------------------------------------------------------------------
// Calculs
// ---------------------------------------------------------------------------
/** Évolution en % ; `null` quand il n'y a pas de base de comparaison. */
function evolution(actuel: number, precedent: number): number | null {
if (!precedent) return null;
return ((actuel - precedent) / Math.abs(precedent)) * 100;
}
function panierMoyen(ca: number, tickets: number): number | null {
return tickets > 0 ? ca / tickets : null;
}
/** « 2026-10-02 » → « vendredi 2 octobre 2026 ». */
function dateLongue(iso: string): string {
const d = new Date(`${iso}T12:00:00`);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
}
// ---------------------------------------------------------------------------
// Blocs
// ---------------------------------------------------------------------------
function Indicateur({ label, valeur, valeurN1, delta }: {
label: string;
valeur: string;
valeurN1: string | null;
delta: number | null;
}) {
return (
{label}
{valeur}
{valeurN1 && N-1 : {valeurN1} }
);
}
function CarteMagasin({ site }: { site: DashboardSiteStats }) {
const panier = panierMoyen(site.ca_hier, site.tickets_hier);
const panierN1 = panierMoyen(site.ca_n1, site.tickets_n1);
return (
{nomMagasin(site.site)}}
/>
0 ? fmtEur2(site.ca_n1) : null}
delta={evolution(site.ca_hier, site.ca_n1)}
/>
0 ? fmtEntier(site.tickets_n1) : null}
delta={evolution(site.tickets_hier, site.tickets_n1)}
/>
);
}
type Critere = "ca" | "qte" | "marge";
const CRITERES: Record = {
ca: { titre: "Meilleur chiffre d'affaires", icone: Euro },
qte: { titre: "Plus grandes quantités", icone: ShoppingCart },
marge: { titre: "Meilleure marge", icone: BarChart3 },
};
function valeurPrincipale(item: DashboardTopItem, critere: Critere): string {
if (critere === "qte") return `${fmtEntier(item.qte)} vendus`;
if (critere === "marge") return fmtEur2(item.marge);
return fmtEur2(item.ca);
}
function ListeTop10({ items, critere }: { items: DashboardTopItem[]; critere: Critere }) {
const { titre, icone: Icone } = CRITERES[critere];
return (
{titre}
{items.length === 0 ? (
Aucune vente enregistrée.
) : (
{items.map((item, i) => {
const taux = item.ca > 0 ? (item.marge / item.ca) * 100 : null;
return (
{i + 1}
{item.libelle1 ?? item.codein}
{valeurPrincipale(item, critere)}
{item.fournisseur}
{critere !== "ca" && CA {fmtEur2(item.ca)} }
{critere !== "qte" && {fmtEntier(item.qte)} vendus }
Stock {fmtEntier(item.stock)}
{taux != null && (
Marge {fmtDecimal1(taux)} %
)}
);
})}
)}
);
}
function Top10Magasin({ siteData }: { siteData: DashboardSiteTop10 }) {
return (
{nomMagasin(siteData.site)}
);
}
function TitreSection({ titre, description }: { titre: string; description?: React.ReactNode }) {
return (
{titre}
{description &&
{description}
}
);
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function DashboardPage() {
let data;
try {
data = await getCachedDashboardData();
} catch (e) {
console.error("[Dashboard] pgGetDashboardData failed:", e);
return (
);
}
const { dateHier, dateN1, sites, top10BySite } = data;
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);
const panier = panierMoyen(totalCa, totalTickets);
const panierN1 = panierMoyen(totalCaN1, totalTicketsN1);
return (
Les ventes de {dateLongue(dateHier)}
{dateN1 && <>, comparées au même jour de la semaine l'an dernier ({dateLongue(dateN1)})>}.
>
}
actions={Données mises à jour chaque nuit }
/>
{sites.length === 0 ? (
) : (
Total de nos magasins, évolution par rapport à .>}
/>
0 ? `N-1 : ${fmtEur2(totalCaN1)}` : undefined}
icon={Euro}
/>
0 ? `N-1 : ${fmtEntier(totalTicketsN1)}` : undefined}
icon={Receipt}
/>
{top10BySite.length > 0 && (
{top10BySite.map((s) => )}
)}
)}
);
}