// ----------------------------------------------------------------------------- // Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/MarketCard.tsx : « Prix et marché » — KPI (prix demandé, médiane des // comparables, fourchette 25–75 %), jauge de position (percentile), nuage // prix / kilométrage des comparables (ce véhicule en orange), PDSF d'origine // si connu, historique du prix demandé, méthodologie. // ----------------------------------------------------------------------------- import { MarketAnalysis, VehicleDetail, fmtDate, fmtPrice } from "../api"; import { Ico } from "../components/Icons"; import { ComparaisonPrix } from "./synthese"; import { PriceCapsule } from "./VehicleHero"; import { Accordion, SectionCard, StatTile, NBSP } from "./ui"; function Scatter({ m, price, km }: { m: MarketAnalysis; price: number; km: number | null }) { const pts = m.points; if (pts.length < 5) return null; const W = 520, H = 220, PAD = { t: 16, r: 14, b: 28, l: 52 }; const kms = pts.map((d) => d.km).concat(km != null ? [km] : []); const ps = pts.map((d) => d.p).concat([price]); const kMin = Math.min(...kms), kMax = Math.max(...kms), pMin = Math.min(...ps), pMax = Math.max(...ps); const x = (v: number) => PAD.l + (kMax > kMin ? (v - kMin) / (kMax - kMin) : 0.5) * (W - PAD.l - PAD.r); const y = (v: number) => H - PAD.b - (pMax > pMin ? (v - pMin) / (pMax - pMin) : 0.5) * (H - PAD.t - PAD.b); const fmtK = (v: number) => `${Math.round(v / 1000)} k`; return ( médiane {fmtPrice(m.median)} {pts.map((d, i) => )} {km != null && ( ce véhicule )} {fmtK(kMin)} km {fmtK(kMax)} km {fmtK(pMax)} $ {fmtK(pMin)} $ ); } export default function MarketCard({ v, cmp }: { v: VehicleDetail; cmp: ComparaisonPrix | null }) { if (v.price == null) return null; const m = v.market; const hist = (v.price_history ?? []).filter((h) => h.price != null); const msrp = typeof v.details?.msrp === "number" ? (v.details.msrp as number) : null; const jours = v.first_seen ? Math.max(0, Math.round((Date.now() / 1000 - v.first_seen) / 86400)) : null; return ( } aside={cmp && }> {m && m.n >= 3 ? ( <>
82 ? "end" : "middle"}>{m.percentile}{NBSP}% des comparables sont moins chers
moins cher · {fmtPrice(m.min)}plus cher · {fmtPrice(m.max)}
{m.n} {v.make} {v.model.split(" ")[0]}{v.year ? ` ${v.year - 1}–${v.year + 1}` : ""} en vente au Québec {msrp != null && msrp > 0 && PDSF d'origine {fmtPrice(msrp)} ({Math.round((1 - v.price / msrp) * 100)}{NBSP}% de moins)} {jours != null && jours > 0 && {jours} jour{jours > 1 ? "s" : ""} en vente (observé)}
) : ( <>
{msrp != null && msrp > 0 && } {jours != null && jours > 0 && }

Pas assez de {v.make} {v.model.split(" ")[0]} comparables en vente pour situer ce prix.

)} {hist.length >= 2 && ( <>

Historique du prix demandé

    {hist.slice(0, 8).map((h, i) => { const prev = hist[i + 1]; const cls = prev && prev.price != null ? (h.price! < prev.price ? "baisse" : h.price! > prev.price ? "hausse" : "") : ""; return (
  • {fmtDate(h.ts)} {prev && prev.price != null && prev.price !== h.price ? <>{h.price! < prev.price ? "Baissé" : "Monté"} de {fmtPrice(prev.price)} à {fmtPrice(h.price!)} : prev ? <>Prix inchangé : {fmtPrice(h.price!)} : <>Premier prix observé : {fmtPrice(h.price!)}}
  • ); })}
)}

Comparables = véhicules de même marque et modèle, année ±1, en vente au Québec dans les sources Auto-Ka, doublons (même NIV) exclus. La médiane et la fourchette 25–75 % sont recalculées à chaque affichage ; le percentile indique la part des comparables affichés moins cher. L'état du véhicule, les options et l'historique d'entretien ne sont pas pris en compte : c'est un repère, pas une évaluation. Le PDSF provient de la fiche constructeur quand il est connu.

); }