Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/MarketPriceCard.tsx : « Prix et marché » — estimation Vrai-Prix5// (valeur, fourchette, confiance) avec jauge SVG (fourchette, estimation,6// prix demandé), écart en %, prix au pi², rôle d'évaluation foncière7// (terrain + bâtiment, année, écart), historique du prix demandé observé par8// Immo-Ka et temps sur le marché, méthodologie en accordéon. Toutes les9// données arrivent avec l'annonce : aucun chargement différé.10// -----------------------------------------------------------------------------11import { Listing, fmtDate, fmtPrice } from "../api";12import { Ico } from "../components/Icons";13import { PriceCapsule } from "./PropertyHero";14import { ComparaisonPrix, estLocation, evaluationMunicipale } from "./synthese";15import { Accordion, SectionCard, StatTile, NBSP } from "./ui";1617const CONF: Record<string, string> = { A: "Confiance élevée", B: "Confiance bonne", C: "Confiance moyenne", D: "Estimation indicative" };1819function Gauge({ lo, hi, est, ask }: { lo: number; hi: number; est: number; ask: number | null }) {20 const W = 320, H = 54;21 const min = Math.min(lo, ask ?? lo) * 0.97, max = Math.max(hi, ask ?? hi) * 1.03;22 const x = (v: number) => 8 + ((Math.min(Math.max(v, min), max) - min) / (max - min)) * (W - 16);23 const anchor = (px: number) => (px < 56 ? "start" : px > W - 56 ? "end" : "middle");24 return (25 <div className="ik-gauge">26 <svg viewBox={`0 0 ${W} ${H}`} role="img"27 aria-label={`Fourchette Vrai-Prix de ${fmtPrice(lo)} à ${fmtPrice(hi)}, estimation ${fmtPrice(est)}${ask != null ? `, prix demandé ${fmtPrice(ask)}` : ""}`}>28 <rect className="ik-gauge-track" x="8" y="24" width={W - 16} height="8" rx="4" />29 <rect className="ik-gauge-box" x={x(lo)} y="22" width={Math.max(4, x(hi) - x(lo))} height="12" rx="4" />30 <line className="ik-gauge-med" x1={x(est)} x2={x(est)} y1="16" y2="40" />31 <text className="ik-gauge-txt" x={x(est)} y="11" textAnchor={anchor(x(est))}>estimation {fmtPrice(est)}</text>32 {ask != null && (33 <>34 <circle className="ik-gauge-me" cx={x(ask)} cy="28" r="6.5" />35 <text className="ik-gauge-txt me" x={x(ask)} y="52" textAnchor={anchor(x(ask))}>prix demandé {fmtPrice(ask)}</text>36 </>37 )}38 </svg>39 <div className="ik-gauge-lbls"><span>{fmtPrice(lo)}</span><span>{fmtPrice(hi)}</span></div>40 </div>41 );42}4344export default function MarketPriceCard({ l, cmp }: { l: Listing; cmp: ComparaisonPrix | null }) {45 if (l.price == null) return null;46 const location = estLocation(l);47 const vp = l.vraiprix && l.vraiprix.value != null && !location ? l.vraiprix : null;48 const ev = evaluationMunicipale(l);49 const hist = (l.price_history ?? []).filter((h) => h.price != null);50 const pi2 = l.area_sqft != null && l.area_sqft > 200 && !location ? Math.round(l.price / l.area_sqft) : null;51 const evPct = ev && !location ? Math.round(((l.price - ev.total) / ev.total) * 100) : null;52 const jours = l.days_on_market ?? null;5354 return (55 <SectionCard id="prix" title="Prix et marché" icon={<Ico name="scale" size={18} />}56 aside={cmp && <PriceCapsule cmp={cmp} short />}>57 {vp ? (58 <>59 <div className="ik-kpis cols-3">60 <StatTile accent value={fmtPrice(l.price)} label="Prix demandé" />61 <StatTile value={fmtPrice(vp.value)} label="Estimation Vrai-Prix" />62 <StatTile value={vp.low != null && vp.high != null ? `${fmtPrice(vp.low)} – ${fmtPrice(vp.high)}` : "—"} label="Fourchette estimée" />63 </div>64 {vp.low != null && vp.high != null && vp.high > vp.low && (65 <Gauge lo={vp.low} hi={vp.high} est={vp.value as number} ask={cmp ? l.price : null} />66 )}67 {!cmp && (68 <p className="ik-note info">Prix demandé trop éloigné de l'estimation pour être comparé (type de bien ou annonce atypique).</p>69 )}70 <div className="ik-meta">71 <span>{(vp.confidence && CONF[vp.confidence]) || "Estimation automatisée"}{vp.confidence ? ` (${vp.confidence})` : ""}</span>72 {pi2 != null && <span><b>{pi2.toLocaleString("fr-CA")}{NBSP}$</b> par pi² habitable</span>}73 {jours != null && jours > 0 && <span><b>{jours}</b> jour{jours > 1 ? "s" : ""} sur le marché (observé)</span>}74 </div>75 </>76 ) : (77 <>78 <div className="ik-kpis cols-3">79 <StatTile accent value={fmtPrice(l.price)} label={location ? "Loyer mensuel" : "Prix demandé"} />80 {pi2 != null && <StatTile value={`${pi2.toLocaleString("fr-CA")}${NBSP}$`} label="Par pi² habitable" />}81 {jours != null && jours > 0 && <StatTile value={jours} label="Jours sur le marché (observé)" />}82 </div>83 {!location && <p className="ik-fine meth">Pas d'estimation Vrai-Prix pour cette annonce (adresse non appariée au rôle ou type de bien non couvert).</p>}84 </>85 )}8687 {ev && !location && (88 <div className="ik-role">89 <h3 className="ik-card-sub ik-subtitle">Rôle d'évaluation foncière{ev.annee ? ` · ${ev.annee}` : ""}</h3>90 <div className="ik-kpis cols-3">91 <StatTile value={fmtPrice(ev.total)} label="Évaluation municipale" anim={false} />92 {ev.terrain != null && <StatTile value={fmtPrice(ev.terrain)} label="Terrain" anim={false} />}93 {ev.batiment != null && <StatTile value={fmtPrice(ev.batiment)} label="Bâtiment" anim={false} />}94 </div>95 {evPct != null && Math.abs(evPct) <= 300 && (96 <p className="ik-fine" style={{ marginTop: 8 }}>97 Prix demandé <b>{Math.abs(evPct)}{NBSP}% {evPct >= 0 ? "au-dessus" : "sous"}</b> l'évaluation municipale — le rôle triennal98 reflète la valeur au 1ᵉʳ juillet de l'année de référence, pas le marché actuel.99 </p>100 )}101 </div>102 )}103104 {hist.length >= 2 && (105 <>106 <h3 className="ik-card-sub ik-subtitle">Historique du prix demandé</h3>107 <ul className="ik-tl">108 {hist.slice(0, 6).map((h, i) => {109 const prev = hist[i + 1];110 const cls = prev && prev.price != null ? (h.price! < prev.price ? "baisse" : "hausse") : "";111 return (112 <li key={h.ts} className={cls}>113 <span className="ik-tl-date">{fmtDate(h.ts)}</span>114 {prev && prev.price != null115 ? <>{h.price! < prev.price ? "Baissé" : "Monté"} de {fmtPrice(prev.price)} à <b>{fmtPrice(h.price!)}</b></>116 : <>Premier prix observé : <b>{fmtPrice(h.price!)}</b></>}117 </li>118 );119 })}120 </ul>121 </>122 )}123124 <Accordion title="Méthodologie" small>125 <p>126 L'estimation Vrai-Prix (service Groupe KA) est produite par un modèle statistique fondé sur les ventes127 comparables récentes, les caractéristiques du bien et le rôle d'évaluation foncière ; la fourchette exprime128 l'incertitude du modèle et la lettre de confiance (A à D) sa fiabilité. Ce n'est pas une évaluation129 agréée. L'historique de prix et le temps sur le marché sont ceux observés par les synchronisations130 Immo-Ka (première observation, pas la date d'inscription à la source).131 {vp && <> <a href={vp.url} target="_blank" rel="noopener noreferrer">Analyse détaillée sur Vrai-Prix ↗</a></>}132 </p>133 </Accordion>134 </SectionCard>135 );136}137