SPB Git

spb/auto-ka Public

Python 82.8% TypeScript 11.9% CSS 5.1%
6.2 KB · 187 lines tsx
Raw Blame History
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