spb/auto-ka Public
Python 82.8%
TypeScript 11.9%
CSS 5.1%
1// -----------------------------------------------------------------------------2// Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// Stats.tsx : tableau de bord du marché — héros, distributions, classements,5// parts de marché, aubaines + rapport PDF téléchargeable.6// -----------------------------------------------------------------------------7import { useEffect, useState } from "react";8import { Link } from "react-router-dom";9import { fmtPrice } from "../api";10import { Datum, Donut, HBars, VBars } from "../components/Charts";1112interface Detailed {13 total: number;14 sources: number;15 regions: number;16 avg_price: number | null;17 median_price: number | null;18 avg_km: number | null;19 median_km: number | null;20 avg_year: number | null;21 new_7d: number;22 electrified_pct: number;23 price_hist: Datum[];24 km_hist: Datum[];25 year_hist: Datum[];26 by_make: { label: string; n: number; avg_price: number | null }[];27 by_region: { label: string; n: number; avg_price: number | null }[];28 by_body: Datum[];29 by_fuel: Datum[];30 top_models: { label: string; n: number; avg_price: number | null; avg_km: number | null }[];31 top_dealers: { label: string; n: number; avg_price: number | null }[];32 price_drops: {33 uid: string; title: string; year: number | null; price: number;34 prev_price: number; dealer_name: string; city: string;35 }[];36}3738const fmtK = (n: number | null) =>39 n == null ? "—" : `${Math.round(n / 1000)}k km`;4041export default function StatsPage() {42 const [s, setS] = useState<Detailed | null>(null);4344 useEffect(() => {45 fetch("/api/stats/detailed")46 .then((r) => r.json())47 .then(setS)48 .catch(() => {});49 }, []);5051 if (!s)52 return (53 <div className="container page">54 <div className="skeleton" style={{ height: 120, marginBottom: 20 }} />55 <div className="skeleton" style={{ height: 340 }} />56 </div>57 );5859 const withPrice = (rows: { label: string; n: number; avg_price: number | null }[]): Datum[] =>60 rows.map((r) => ({61 label: r.label, n: r.n,62 extra: r.avg_price ? `moy. ${fmtPrice(r.avg_price)}` : undefined,63 }));6465 return (66 <div className="container page">67 <div className="stats-head">68 <div>69 <span className="kicker">Marché</span>70 <h1>Le marché de l'occasion, en direct</h1>71 <p className="lead" style={{ marginBottom: 0 }}>72 Calculé en temps réel sur l'inventaire actif de {s.sources} concessionnaires73 dans {s.regions} régions du Québec.74 </p>75 </div>76 <a className="btn pdf-btn" href="/api/stats/rapport.pdf">77 ⬇ Télécharger le rapport PDF78 </a>79 </div>8081 <div className="stat-grid" style={{ marginTop: 26 }}>82 <div className="stat-tile hero-tile">83 <b>{s.total.toLocaleString("fr-CA")}</b>84 <span>véhicules en vente</span>85 </div>86 <div className="stat-tile">87 <b>{fmtPrice(s.avg_price)}</b>88 <span>prix moyen</span>89 </div>90 <div className="stat-tile">91 <b>{fmtPrice(s.median_price)}</b>92 <span>prix médian</span>93 </div>94 <div className="stat-tile">95 <b>{fmtK(s.avg_km)}</b>96 <span>km moyen</span>97 </div>98 <div className="stat-tile">99 <b>{s.new_7d.toLocaleString("fr-CA")}</b>100 <span>arrivages · 7 jours</span>101 </div>102 <div className="stat-tile">103 <b>{s.electrified_pct} %</b>104 <span>électrifiés (VÉ + hybrides)</span>105 </div>106 </div>107108 <div className="panel">109 <h3>💰 Distribution des prix</h3>110 <VBars data={s.price_hist} color="#d94f1e" />111 </div>112113 <div className="vd-cols">114 <div className="panel">115 <h3>🛣 Kilométrage (milliers de km)</h3>116 <VBars data={s.km_hist} color="#1f6fb5" height={170} />117 </div>118 <div className="panel">119 <h3>📅 Années-modèles</h3>120 <VBars data={s.year_hist} color="#b58500" height={170} />121 </div>122 </div>123124 <div className="vd-cols">125 <div className="panel">126 <h3>🏷 Top marques</h3>127 <HBars data={withPrice(s.by_make)} color="#d94f1e" />128 </div>129 <div className="panel">130 <h3>📍 Par région</h3>131 <HBars data={withPrice(s.by_region)} color="#1f6fb5" />132 </div>133 </div>134135 <div className="vd-cols">136 <div className="panel">137 <h3>🚙 Carrosseries</h3>138 <Donut data={s.by_body} />139 </div>140 <div className="panel">141 <h3>⛽ Carburants</h3>142 <Donut data={s.by_fuel} />143 </div>144 </div>145146 <div className="panel">147 <h3>🚗 Modèles les plus offerts</h3>148 <HBars149 data={s.top_models.map((m) => ({150 label: m.label, n: m.n,151 extra: `moy. ${fmtPrice(m.avg_price)} · ${fmtK(m.avg_km)}`,152 }))}153 color="#1c7a4d"154 />155 </div>156157 {s.price_drops.length > 0 && (158 <section style={{ margin: "30px 0" }}>159 <h2 style={{ marginBottom: 14 }}>📉 Baisses de prix récentes</h2>160 <div className="src-grid">161 {s.price_drops.map((d) => (162 <Link key={d.uid} to={`/vehicule/${encodeURIComponent(d.uid)}`} className="src-card">163 <h3>{d.title}</h3>164 <div className="meta">{d.dealer_name} · {d.city}</div>165 <div className="n">166 {fmtPrice(d.price)}{" "}167 <span style={{ fontSize: 13, color: "var(--ink-3)", textDecoration: "line-through" }}>168 {fmtPrice(d.prev_price)}169 </span>170 </div>171 <div className="meta" style={{ color: "var(--good)", fontWeight: 700 }}>172 −{fmtPrice(d.prev_price - d.price)}173 </div>174 </Link>175 ))}176 </div>177 </section>178 )}179180 <div className="panel">181 <h3>🏢 Plus grands inventaires</h3>182 <HBars data={withPrice(s.top_dealers)} color="#7d4fc9" />183 </div>184 </div>185 );186}187