// ============================================================================== // Author: Simon-Pierre Boucher // File: pages/Stats.tsx // Desc: Tableau de bord analytique — module Stats commun Groupe KA v2 // (ka-ui/stats/SPEC.md §1) : bandeau KPI avec sparklines, sélecteur // de période, jauges de complétude, courbes/aires/barres + stats de // séries, multi-courbes (prix par ville), barres empilées (ajouts par // source), répartitions, distributions, géographie, heatmaps // calendrier + horaire, tableaux, records, export PDF (5 rapports). // Données 100 % réelles servies par /api/stats/dashboard (cache 5 min). // ============================================================================== import { useCallback, useEffect, useState } from "react"; import { BarChart, BreakItem, CalendarHeatmap, DataTable, Distribution, Donut, EmptyBlock, Fraicheur, Gauge, GaugeCard, Histogram, HourCell, HourHeatmap, Kpi, KpiCard, LineChart, MultiLineChart, MultiSerie, PdfButton, PeriodSelector, RecordCard, RecordFact, Serie, StackedBarChart, StackedSerie, StatSummary, TableSpec, } from "../ka/stats/kacharts"; /* ---------- contrat /api/stats/dashboard (SPEC.md v2 §2) ---------- */ type Breakdown = { id: string; title: string; kind?: "donut" | "bar"; items: BreakItem[] }; type Dashboard = { updated: string; period: { from: string; to: string; label: string }; kpis?: Kpi[]; gauges?: Gauge[]; series?: Serie[]; multiseries?: MultiSerie[]; stacked?: StackedSerie[]; breakdowns?: Breakdown[]; distributions?: Distribution[]; geo?: { title: string; items: BreakItem[] }; heatmap?: { title: string; cells: { date: string; value: number }[] }; hourly?: { title: string; cells: HourCell[] }; tables?: TableSpec[]; records?: RecordFact[]; }; async function fetchDashboard(period: string, from?: string, to?: string): Promise { const p = new URLSearchParams({ period }); if (from && to) { p.set("from", from); p.set("to", to); } const res = await fetch(`/api/stats/dashboard?${p}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } export default function StatsPage() { const [period, setPeriod] = useState("30j"); const [custom, setCustom] = useState<{ from: string; to: string }>({ from: "", to: "" }); const [dash, setDash] = useState(null); const [error, setError] = useState(false); const [loading, setLoading] = useState(true); const load = useCallback(() => { setLoading(true); const useCustom = custom.from && custom.to; fetchDashboard(period, useCustom ? custom.from : undefined, useCustom ? custom.to : undefined) .then((d) => { setDash(d); setError(false); }) .catch(() => setError(true)) .finally(() => setLoading(false)); }, [period, custom]); useEffect(() => { load(); }, [load]); if (error) { return (

Stats indisponibles

Le tableau de bord n'a pas pu être chargé. Réessayez dans un instant.

); } if (!dash) return

Chargement…

; const donuts = (dash.breakdowns ?? []).filter((b) => b.kind === "donut"); const bars = (dash.breakdowns ?? []).filter((b) => b.kind !== "donut"); // séries clés (volumes quotidiens) : stats min/max/moy/méd/σ dessous const withStats = new Set(["new_restos", "releves"]); return (
{/* ---- en-tête : titre + PDF (5 rapports) + fraîcheur (SPEC §1.10-11) ---- */}
Statistiques · {dash.period.label}

La table du Québec, en chiffres

Tableau de bord analytique en direct — restos référencés, menus, plats & prix suivis. Données réelles, rien d'inventé.

{/* ---- 1. bandeau KPI (sparklines incluses) ---- */} {dash.kpis?.length ? (
{dash.kpis.map((k) => )}
) : } {/* ---- 2. sélecteur de période global ---- */}
{ setPeriod(p); setCustom({ from: "", to: "" }); }} custom={custom} onCustom={(from, to) => setCustom({ from, to })} />
{/* ---- 3. jauges : couverture & complétude des fiches ---- */} {dash.gauges?.length ? (
Couverture & complétude
{dash.gauges.map((g) => )}
) : null} {/* ---- 4. évolution : courbes / aires / barres + stats de séries ---- */}
{(dash.series ?? []).map((s) => (
{withStats.has(s.id) && }
))}
{/* ---- 4b. multi-courbes (≤ 4 séries, motifs distincts) + empilées ---- */} {(dash.multiseries?.length || dash.stacked?.length) ? (
{(dash.multiseries ?? []).map((ms) => )} {(dash.stacked ?? []).map((st) => )}
) : null} {/* ---- 5. répartitions : anneaux + barres + distributions ---- */}
{donuts.map((b) => )} {bars.map((b) => )} {(dash.distributions ?? []).map((d) => )} {/* ---- 6. répartition géographique ---- */} {dash.geo ? : }
{/* ---- 7. calendriers : chaleur 26 semaines + activité horaire 7×24 ---- */} {dash.heatmap && } {dash.hourly && } {/* ---- 8. tableaux détaillés ---- */} {(dash.tables ?? []).map((t) => )} {/* ---- 9. records & faits marquants ---- */} {dash.records?.length ? (
Records & faits marquants
{dash.records.map((r) => )}
) : null}
Les prix appartiennent aux restaurants et à leurs plateformes ; Resto-Ka les agrège pour la découverte et renvoie toujours à la source. Rapports PDF estampillés Groupe-KA (complet, synthèse, tendances, répartitions, données) disponibles en haut de page.
); }