SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.2 KB · 100 lines tsx
Raw Blame History
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