Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/MarketPriceCard.tsx : « Prix et marché » — juste valeur estimée5// (fairvalue.py) : valeur, fourchette, écart, confiance, histogramme du6// segment (ce loyer en orange, juste valeur en tiret), méthodologie en7// accordéon. Données : Res<FairValueDetail> (squelette / vide / erreur).8// -----------------------------------------------------------------------------9import { Link } from "react-router-dom";10import { FairValueDetail, Listing, fmtPrice } from "../api";11import { IcoScale } from "../components/Icons";12import { PriceCapsule } from "./PropertyHero";13import { ComparaisonPrix } from "./synthese";14import { Accordion, ErrorState, SectionCard, SkeletonLines, StatTile, NBSP } from "./ui";15import { Res } from "./useFicheData";1617const CONF = { fort: "Confiance forte", moyen: "Confiance moyenne", faible: "Estimation indicative" } as const;1819function Histo({ d, price }: { d: FairValueDetail; price: number }) {20 const bins = d.histogram;21 if (!bins || bins.length < 4) return null;22 const W = 320, H = 92, top = 18, bottom = 16;23 const lo = bins[0].x0, hi = bins[bins.length - 1].x1;24 if (hi <= lo) return null;25 const max = Math.max(...bins.map((b) => b.n), 1);26 const x = (v: number) => ((Math.min(Math.max(v, lo), hi) - lo) / (hi - lo)) * W;27 const bw = W / bins.length;28 const plotH = H - top - bottom;29 const priceX = x(price), fvX = x(d.fv);30 const close = Math.abs(priceX - fvX) < 64;31 const anchor = (px: number) => (px < 56 ? "start" : px > W - 56 ? "end" : "middle");32 return (33 <svg className="lk-histo" viewBox={`0 0 ${W} ${H}`} role="img"34 aria-label={`Position du loyer (${fmtPrice(price)}) parmi ${d.segment_n} annonces comparables`}>35 {bins.map((b, i) => {36 const h = Math.max(1.5, (b.n / max) * plotH);37 const on = price >= b.x0 && price < b.x1;38 return (39 <rect key={i} className={`lk-histo-bar ${on ? "on" : ""}`} x={i * bw + 1} y={H - bottom - h}40 rx="2" width={Math.max(1, bw - 2)} height={h}>41 <title>{`${b.x0} $ – ${b.x1} $ : ${b.n} annonce${b.n > 1 ? "s" : ""}`}</title>42 </rect>43 );44 })}45 <rect x={x(d.fv_low)} y={H - bottom} width={Math.max(2, x(d.fv_high) - x(d.fv_low))} height="3" rx="1.5" fill="#c9cac4" />46 <line x1={fvX} x2={fvX} y1={top - 2} y2={H - bottom} stroke="#4d5551" strokeWidth="1.5" strokeDasharray="3 3" />47 {!close && <text x={fvX} y={top - 7} textAnchor={anchor(fvX)} className="lk-histo-lbl fv">Juste valeur</text>}48 <line x1={priceX} x2={priceX} y1={top - 2} y2={H - bottom} stroke="#141814" strokeWidth="2" />49 <text x={priceX} y={close ? top - 7 : H - 4} textAnchor={anchor(priceX)} className="lk-histo-lbl">Ce loyer{close ? " / juste valeur" : ""}</text>50 <text x="1" y={H - 4} className="lk-histo-axis" textAnchor="start">{lo} $</text>51 <text x={W - 1} y={H - 4} className="lk-histo-axis" textAnchor="end">{hi} $</text>52 </svg>53 );54}5556export default function MarketPriceCard({ l, fv, cmp, onRetry }: {57 l: Listing; fv: Res<FairValueDetail>; cmp: ComparaisonPrix | null; onRetry: () => void;58}) {59 if (l.price == null) return null;60 const d = fv.status === "ok" ? fv.data : null;61 return (62 <SectionCard id="prix" title="Prix et marché" icon={<IcoScale size={18} />}63 aside={cmp && <PriceCapsule cmp={cmp} short />}>64 {fv.status === "loading" && <SkeletonLines n={4} />}65 {fv.status === "error" && <ErrorState onRetry={onRetry}>Analyse de prix temporairement indisponible.</ErrorState>}66 {(fv.status === "na" || (d && d.verdict == null)) && !d && (67 <p className="lk-fine meth">Pas d'analyse de prix pour cette annonce (loyer absent ou segment sans comparables).</p>68 )}69 {d && (70 <>71 <div className="lk-kpis cols-3">72 <StatTile accent value={fmtPrice(l.price)} label="Loyer demandé" />73 <StatTile value={fmtPrice(d.fv)} label="Juste valeur estimée" />74 <StatTile value={`${fmtPrice(d.fv_low)} – ${fmtPrice(d.fv_high)}`} label="Fourchette estimée" />75 </div>76 {d.verdict == null && (77 <p className="lk-note info">78 {CONF[d.confidence]} — pas assez de comparables fiables pour classer ce loyer.79 </p>80 )}81 <Histo d={d} price={l.price} />82 <div className="lk-meta">83 <span>{CONF[d.confidence]}</span>84 <span><b>{d.segment_n.toLocaleString("fr-CA")}</b> annonces comparables</span>85 {d.comps > 0 && <span><b>{d.comps}</b> voisines retenues</span>}86 </div>87 <Accordion title="Méthodologie de la juste valeur" small>88 <p>89 Estimation indicative recalculée en continu à partir des annonces comparables du marché90 (même segment{NBSP}: nombre de chambres, ville, période), méthode {d.method}, modèle {d.model_version}.91 Ce n'est pas une évaluation officielle.{" "}92 <Link to="/juste-valeur">Comment est calculée la juste valeur ?</Link>93 </p>94 </Accordion>95 </>96 )}97 </SectionCard>98 );99}100