SPB Git forge

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

52commits 1branches 0releases
11.6 MBsize
maindefault branch
20 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%
7.6 KB · 182 lines tsx
Raw Blame History
1// ==============================================================================2// Author: Simon-Pierre Boucher <contact@spboucher.ai>3// File:   pages/Stats.tsx4// Desc:   Tableau de bord analytique — module Stats commun Groupe KA v25//         (ka-ui/stats/SPEC.md §1) : bandeau KPI avec sparklines, sélecteur6//         de période, jauges de complétude, courbes/aires/barres + stats de7//         séries, multi-courbes (prix par ville), barres empilées (ajouts par8//         source), répartitions, distributions, géographie, heatmaps9//         calendrier + horaire, tableaux, records, export PDF (5 rapports).10//         Données 100 % réelles servies par /api/stats/dashboard (cache 5 min).11// ==============================================================================12import { useCallback, useEffect, useState } from "react";13import {14  BarChart, BreakItem, CalendarHeatmap, DataTable, Distribution, Donut,15  EmptyBlock, Fraicheur, Gauge, GaugeCard, Histogram, HourCell, HourHeatmap,16  Kpi, KpiCard, LineChart, MultiLineChart, MultiSerie, PdfButton,17  PeriodSelector, RecordCard, RecordFact, Serie, StackedBarChart,18  StackedSerie, StatSummary, TableSpec,19} from "../ka/stats/kacharts";2021/* ---------- contrat /api/stats/dashboard (SPEC.md v2 §2) ---------- */22type Breakdown = { id: string; title: string; kind?: "donut" | "bar"; items: BreakItem[] };23type Dashboard = {24  updated: string;25  period: { from: string; to: string; label: string };26  kpis?: Kpi[];27  gauges?: Gauge[];28  series?: Serie[];29  multiseries?: MultiSerie[];30  stacked?: StackedSerie[];31  breakdowns?: Breakdown[];32  distributions?: Distribution[];33  geo?: { title: string; items: BreakItem[] };34  heatmap?: { title: string; cells: { date: string; value: number }[] };35  hourly?: { title: string; cells: HourCell[] };36  tables?: TableSpec[];37  records?: RecordFact[];38};3940async function fetchDashboard(period: string, from?: string, to?: string): Promise<Dashboard> {41  const p = new URLSearchParams({ period });42  if (from && to) { p.set("from", from); p.set("to", to); }43  const res = await fetch(`/api/stats/dashboard?${p}`);44  if (!res.ok) throw new Error(`HTTP ${res.status}`);45  return res.json();46}4748export default function StatsPage() {49  const [period, setPeriod] = useState("30j");50  const [custom, setCustom] = useState<{ from: string; to: string }>({ from: "", to: "" });51  const [dash, setDash] = useState<Dashboard | null>(null);52  const [error, setError] = useState(false);53  const [loading, setLoading] = useState(true);5455  const load = useCallback(() => {56    setLoading(true);57    const useCustom = custom.from && custom.to;58    fetchDashboard(period, useCustom ? custom.from : undefined, useCustom ? custom.to : undefined)59      .then((d) => { setDash(d); setError(false); })60      .catch(() => setError(true))61      .finally(() => setLoading(false));62  }, [period, custom]);6364  useEffect(() => { load(); }, [load]);6566  if (error) {67    return (68      <div className="notice container">69        <h2>Stats indisponibles</h2>70        <p>Le tableau de bord n'a pas pu être chargé. Réessayez dans un instant.</p>71      </div>72    );73  }74  if (!dash) return <div className="notice container"><p>Chargement…</p></div>;7576  const donuts = (dash.breakdowns ?? []).filter((b) => b.kind === "donut");77  const bars = (dash.breakdowns ?? []).filter((b) => b.kind !== "donut");78  // séries clés (volumes quotidiens) : stats min/max/moy/méd/σ dessous79  const withStats = new Set(["new_restos", "releves"]);8081  return (82    <div className="container stats-page" style={{ opacity: loading ? 0.55 : 1, transition: "opacity 0.15s" }}>83      {/* ---- en-tête : titre + PDF (5 rapports) + fraîcheur (SPEC §1.10-11) ---- */}84      <div className="stats-head">85        <div>86          <span className="kicker">Statistiques · {dash.period.label}</span>87          <h1 className="stats-title">La table du Québec, en chiffres</h1>88          <p className="sub">89            Tableau de bord analytique en direct — restos référencés, menus,90            plats & prix suivis. Données réelles, rien d'inventé.91          </p>92        </div>93        <div className="stats-actions">94          <PdfButton period={period}95            from={custom.from && custom.to ? custom.from : undefined}96            to={custom.from && custom.to ? custom.to : undefined} />97          <Fraicheur updated={dash.updated} onRefresh={load} />98        </div>99      </div>100101      {/* ---- 1. bandeau KPI (sparklines incluses) ---- */}102      {dash.kpis?.length ? (103        <div className="kpi-grid">104          {dash.kpis.map((k) => <KpiCard key={k.id} k={k} />)}105        </div>106      ) : <EmptyBlock title="Indicateurs" />}107108      {/* ---- 2. sélecteur de période global ---- */}109      <div className="card period-bar">110        <PeriodSelector111          value={period}112          onChange={(p) => { setPeriod(p); setCustom({ from: "", to: "" }); }}113          custom={custom}114          onCustom={(from, to) => setCustom({ from, to })}115        />116      </div>117118      {/* ---- 3. jauges : couverture & complétude des fiches ---- */}119      {dash.gauges?.length ? (120        <section>121          <span className="kicker">Couverture & complétude</span>122          <div className="kpi-grid">123            {dash.gauges.map((g) => <GaugeCard key={g.id} g={g} />)}124          </div>125        </section>126      ) : null}127128      {/* ---- 4. évolution : courbes / aires / barres + stats de séries ---- */}129      <div className="stats-grid2">130        {(dash.series ?? []).map((s) => (131          <div key={s.id}>132            <LineChart serie={s} />133            {withStats.has(s.id) && <StatSummary serie={s} />}134          </div>135        ))}136      </div>137138      {/* ---- 4b. multi-courbes (≤ 4 séries, motifs distincts) + empilées ---- */}139      {(dash.multiseries?.length || dash.stacked?.length) ? (140        <div className="stats-grid2">141          {(dash.multiseries ?? []).map((ms) => <MultiLineChart key={ms.id} ms={ms} />)}142          {(dash.stacked ?? []).map((st) => <StackedBarChart key={st.id} st={st} />)}143        </div>144      ) : null}145146      {/* ---- 5. répartitions : anneaux + barres + distributions ---- */}147      <div className="stats-grid2">148        {donuts.map((b) => <Donut key={b.id} title={b.title} items={b.items} />)}149        {bars.map((b) => <BarChart key={b.id} title={b.title} items={b.items} />)}150        {(dash.distributions ?? []).map((d) => <Histogram key={d.id} dist={d} />)}151        {/* ---- 6. répartition géographique ---- */}152        {dash.geo ? <BarChart title={dash.geo.title} items={dash.geo.items} unit="restos" />153          : <EmptyBlock title="Répartition géographique" />}154      </div>155156      {/* ---- 7. calendriers : chaleur 26 semaines + activité horaire 7×24 ---- */}157      {dash.heatmap && <CalendarHeatmap title={dash.heatmap.title} cells={dash.heatmap.cells} />}158      {dash.hourly && <HourHeatmap title={dash.hourly.title} cells={dash.hourly.cells} />}159160      {/* ---- 8. tableaux détaillés ---- */}161      {(dash.tables ?? []).map((t) => <DataTable key={t.id} spec={t} />)}162163      {/* ---- 9. records & faits marquants ---- */}164      {dash.records?.length ? (165        <section>166          <span className="kicker">Records & faits marquants</span>167          <div className="records-grid">168            {dash.records.map((r) => <RecordCard key={r.label} r={r} />)}169          </div>170        </section>171      ) : null}172173      <div className="stats-foot">174        Les prix appartiennent aux restaurants et à leurs plateformes ;175        Resto-Ka les agrège pour la découverte et renvoie toujours à la source.176        Rapports PDF estampillés Groupe-KA (complet, synthèse, tendances,177        répartitions, données) disponibles en haut de page.178      </div>179    </div>180  );181}182