/**
* =============================================================================
* Job·Ka — Groupe KA
* Auteur : Simon-Pierre Boucher
* Contact : contact@spboucher.ai
* Fichier : frontend/src/pages/Stats.tsx
* Rôle : Tableau de bord analytique — module Stats commun Groupe KA v2
* (contrat ../ka/stats/SPEC.md, composants ../ka/stats/kacharts.tsx).
* PDF (5 rapports) + fraîcheur, KPI + sparklines, jauges, courbes
* (N-1) + stats de séries, multi-courbes, barres empilées,
* répartitions, distributions, géographie, calendrier + heatmap
* horaire, tableaux, records.
* Créé : 2026-08-17 Modifié : 2026-08-19
* =============================================================================
*/
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";
interface Dashboard {
updated: string;
period: { from: string | null; to: string | null; label: string };
kpis: Kpi[];
gauges?: Gauge[];
series: Serie[];
multiseries?: MultiSerie[];
stacked?: StackedSerie[];
breakdowns: { id: string; title: string; kind?: string; items: BreakItem[] }[];
distributions?: Distribution[];
geo?: { title: string; items: BreakItem[] };
heatmap?: { title: string; cells: { date: string; value: number }[] };
hourly?: { title: string; cells: HourCell[] };
tables: TableSpec[];
records: RecordFact[];
}
function SectionTitle({ children }: { children: string }) {
return (
{children}
);
}
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 [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
const customOk = Boolean(custom.from && custom.to);
const load = useCallback(() => {
setLoading(true);
setErr(null);
const p = new URLSearchParams({ period });
if (customOk) {
p.set("from", custom.from);
p.set("to", custom.to);
}
fetch(`/api/stats/dashboard?${p}`)
.then((r) => {
if (!r.ok) throw new Error(`API ${r.status}`);
return r.json();
})
.then((d: Dashboard) => setDash(d))
.catch((e) => setErr(String(e?.message ?? e)))
.finally(() => setLoading(false));
}, [period, custom.from, custom.to, customOk]);
useEffect(() => {
document.title = "Statistiques de l'emploi — Job-Ka · Un service Groupe KA";
load();
}, [load]);
const donuts = dash?.breakdowns?.filter((b) => b.kind === "donut") ?? [];
const barBreaks = dash?.breakdowns?.filter((b) => b.kind !== "donut") ?? [];
return (
{/* --- en-tête : titre + PDF (5 rapports) + fraîcheur ------------------ */}
{err && (
Impossible de charger les statistiques.
{err}
)}
{/* --- bandeau KPI (sparklines) ---------------------------------------- */}
{dash && dash.kpis.length > 0 && (
)}
{!dash && loading && (
Chargement des statistiques…
)}
{/* --- sélecteur de période --------------------------------------------- */}
{ setPeriod(p); setCustom({ from: "", to: "" }); }}
custom={custom}
onCustom={(from, to) => setCustom({ from, to })}
/>
{dash && (
{/* --- jauges (complétude des fiches) ------------------------------ */}
{dash.gauges && dash.gauges.length > 0 && (
{dash.gauges.map((g) => )}
)}
{/* --- évolutions : courbes / barres + stats de séries -------------- */}
{dash.series?.length ? (
dash.series.map((s) => (
))
) : (
)}
{/* --- multi-courbes (top secteurs) ---------------------------------- */}
{dash.multiseries?.map((ms) =>
)}
{/* --- barres empilées (ajouts par source) --------------------------- */}
{dash.stacked?.map((st) =>
)}
{/* --- répartitions (anneaux + barres avec deltas) ------------------- */}
{(donuts.length > 0 || barBreaks.length > 0) && (
{donuts.map((b) => )}
{barBreaks.map((b) => )}
)}
{/* --- distributions (histogrammes) ---------------------------------- */}
{dash.distributions && dash.distributions.length > 0 && (
{dash.distributions.map((d) => )}
)}
{/* --- géographie ------------------------------------------------------ */}
{dash.geo &&
}
{/* --- calendrier de chaleur + activité horaire ------------------------ */}
{dash.heatmap &&
}
{dash.hourly &&
}
{/* --- tableaux détaillés ---------------------------------------------- */}
{dash.tables?.map((t) =>
)}
{/* --- records & faits marquants ---------------------------------------- */}
{dash.records?.length > 0 && (
Records & faits marquants
{dash.records.map((r) => )}
)}
)}
);
}