// Auteur : Simon-Pierre Boucher — contact@spboucher.ai /** * ResultView — le « rapport de valeur » : l'écran central du produit. * Composition éditoriale v2 : masthead de rapport, estimation en très grands * chiffres, sections numérotées à filets (registre, calcul, marché, historique, * comparables), zéro carte — la hiérarchie vient de la typographie et des filets. */ "use client"; import { useState } from "react"; import Link from "next/link"; import { useLang } from "./LangContext"; import LeadForm from "./LeadForm"; import RadarMap from "./RadarMap"; import FavButton from "./FavButton"; import { BuildingGlyph, ConfidenceDial, CountUp, EraLine, locNum, LotDiagram, MarketIndexChart, PercentileBar, RangeBar, ValueSplit, } from "./MetricViz"; import type { EstimateResult } from "@/lib/engine"; import type { UnitEstimate } from "@/lib/estimator"; import type { TKey } from "@/lib/i18n"; const fmt = (v: number | null | undefined, lang: string) => v == null ? "—" : new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0, }).format(v); const signed = (v: number, lang: string) => `${v >= 0 ? "+" : "−"}${fmt(Math.abs(Math.round(v)), lang)}`; function Chip({ k, v }: { k: string; v: string | number }) { return (

{k}

{v}

); } /** En-tête de section du rapport : filet fort + numéro mono rouge. */ function Sect({ num, kicker, title }: { num: string; kicker: string; title: string }) { return (
{num} — {kicker}

{title}

); } export default function ResultView({ data }: { data: UnitEstimate }) { const { lang, t } = useLang(); const [activeComp, setActiveComp] = useState(null); const r: EstimateResult = data.result; const u = data.unit; const ctx = data.context; const s = u?.specs; const fr = lang === "fr"; const histMax = Math.max(...(u?.history.map((h) => h.value ?? 0) ?? [0]), 1); const moneyFmt = (v: number) => new Intl.NumberFormat(fr ? "fr-CA" : "en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0, }).format(Math.round(v / 100) * 100); return (
{/* ================= masthead du rapport ================= */}
{u && ( 0 ? moneyFmt(r.estimate) : "", url: `https://www.vrai-prix.com/estimation/${encodeURIComponent(u.id)}`, }} /> )}
{/* colonne principale : adresse + estimation en très grands chiffres */}
{u && (

{u.adresse} {s?.apt ? ` app. ${s.apt}` : ""} {u.municipalite ? · {u.municipalite} : null}

)}

{t("estimatedValue")}

{/* faits rapides — ligne mono, pas de pastilles */}

{[ u ? t(u.typeProp as TKey) : null, u?.aireEtagesM2 && r.estimate > 0 ? `${locNum(Math.round(r.estimate / u.aireEtagesM2), lang, { maxFrac: 0 })} $/m²` : null, u?.aireEtagesM2 ? locNum(u.aireEtagesM2, lang, { unit: "m²" }) : null, u?.anneeConstruction ? `${fr ? "constr." : "built"} ${u.anneeConstruction}` : null, u?.superficieTerrainM2 ? `${fr ? "terrain" : "lot"} ${locNum(u.superficieTerrainM2, lang, { unit: "m²" })}` : null, ] .filter((x): x is string => x != null) .join(" · ")}

{/* fourchette visuelle P10-P90 avec position du rôle */}

{t("range")}

{u && ( )}
{/* colonne de lecture : confiance + rôle + écart, en cellules de données */}

{t("disclaimer")}

{/* ================= 01 — portrait du registre ================= */} {u && s && (
{/* Bâtiment */}

{fr ? "Bâtiment" : "Building"}

{/* Terrain */}

{fr ? "Terrain — à l'échelle" : "Lot — to scale"}

= 1 && !u.superficieTerrainM2)} />
{/* Valeur au rôle */}

{fr ? "Valeur au rôle 2026 — composition" : "2026 assessed value — split"}

{fmt(u.valeurRole, lang)}

{/* registre brut complet */}
▸ {fr ? "Toutes les données du registre (brutes)" : "All registry data (raw)"}
)} {/* ================= 02 — transparence du calcul ================= */}

{t("modelPart")}

{r.modelEstimate != null ? ( ) : ( "—" )}

{t("weightModel")} : {(r.modelWeight * 100).toFixed(0)} %

{t("compsPart")}

{r.compsEstimate != null ? ( ) : ( "—" )}

{t("weightComps")} : {((1 - r.modelWeight) * 100).toFixed(0)} % · {r.nCompsUsed} comps {r.compsDispersionPct != null ? ` · ±${r.compsDispersionPct} %` : ""}

{/* ================= 03 — marché local ================= */} {ctx && (ctx.index.length >= 6 || ctx.sector || ctx.muniPercentile != null) && (
{/* trajectoire du marché */} {ctx.index.length >= 6 && (

{fr ? `Indice de marché — ${t(ctx.typeProp as TKey)} (base 100 en ${ctx.index[0].month.slice(0, 4)})` : `Market index — ${t(ctx.typeProp as TKey)} (base 100 in ${ctx.index[0].month.slice(0, 4)})`}

{ctx.index12mPct != null && ( 12 {fr ? "mois" : "mo"}{" "} = 0 ? "text-ink" : "text-[var(--danger)]"}> {ctx.index12mPct >= 0 ? "+" : ""} {ctx.index12mPct.toFixed(1)} % )} {ctx.indexSince2021Pct != null && ( {ctx.index[0].month.slice(0, 4)}→{" "} = 0 ? "text-ink" : "text-[var(--danger)]"}> {ctx.indexSince2021Pct >= 0 ? "+" : ""} {ctx.indexSince2021Pct.toFixed(0)} % )}

{fr ? "Indice utilisé pour l'ajustement « marché » des comparables — même donnée, même calcul." : "Index used for the comparables' market adjustment — same data, same math."}

)}
{/* ventes du secteur */} {ctx.sector && (

{fr ? `Ventes réelles — rayon ${ctx.sector.radiusKm} km · 12 mois` : `Real sales — ${ctx.sector.radiusKm} km radius · 12 months`}

{fr ? "Ventes" : "Sales"}

{ctx.sector.nSales12m}

{fr ? "Prix médian" : "Median price"}

{ctx.sector.medianPrice != null ? fmt(Math.round(ctx.sector.medianPrice), lang) : "—"}

$/m² {fr ? "médian" : "median"}

{ctx.sector.medianPpm2 != null ? locNum(Math.round(ctx.sector.medianPpm2), lang, { maxFrac: 0 }) : "—"}

{ctx.sector.medianPrice != null && r.estimate > 0 && (

{fr ? "Cette propriété" : "This property"} :{" "} {r.estimate >= ctx.sector.medianPrice ? "+" : "−"} {Math.abs((r.estimate / ctx.sector.medianPrice - 1) * 100).toFixed(0)} % {" "} {fr ? "vs médiane du secteur" : "vs sector median"}

)}
)} {/* position dans la municipalité */} {ctx.muniPercentile != null && u?.municipalite && (

{fr ? `Position dans ${u.municipalite}` : `Position in ${u.municipalite}`}

{fr ? ( <> Vaut plus que{" "} {Math.round(ctx.muniPercentile)} % des{" "} {ctx.muniTotal != null ? locNum(ctx.muniTotal, lang, { maxFrac: 0 }) : ""}{" "} propriétés estimées de la municipalité. ) : ( <> Worth more than{" "} {Math.round(ctx.muniPercentile)} % of the{" "} {ctx.muniTotal != null ? locNum(ctx.muniTotal, lang, { maxFrac: 0 }) : ""}{" "} estimated properties in the municipality. )}

)}
)} {/* ================= 04 — historique ================= */} {u && u.history.some((h) => h.value != null) && (
{u.history.map((h, i) => { const prev = i > 0 ? u.history[i - 1].value : null; const yoy = h.value != null && prev != null && prev > 0 ? ((h.value / prev - 1) * 100) : null; return (
{h.year} {fmt(h.value, lang)} {yoy != null && ( = 0 ? "text-ink-2" : "text-[var(--danger)]"}`}> {yoy >= 0 ? "+" : ""} {yoy.toFixed(0)} % )}
); })}
)} {/* ================= 05 — comparables ================= */}
({ id: c.id, lat: c.lat, lng: c.lng, label: `${c.street ?? ""}, ${c.city ?? ""}`, price: fmt(c.amount, lang), adjusted: fmt(Math.round(c.adjustedPrice), lang), date: c.date, distanceM: c.distanceM, weight: c.weight, }))} highlight={activeComp} onHover={setActiveComp} />
{r.comps.map((c, i) => ( setActiveComp(c.id)} onMouseLeave={() => setActiveComp(null)} className={activeComp === c.id ? "!bg-surface-2" : ""} > ))}
# {fr ? "Vente" : "Sale"} {t("adjusted")}
{i + 1}

{c.street}, {c.city}

{c.date} · {c.distanceM < 1000 ? `${Math.round(c.distanceM)} m` : `${(c.distanceM / 1000).toFixed(1)} km`} ·{" "} {t("sold")} {fmt(c.amount, lang)} {c.floorArea && c.floorArea > 20 ? ` (${locNum(Math.round(c.amount / c.floorArea), lang, { maxFrac: 0 })} $/m²)` : ""}

{(c.floorArea || c.yearBuilt) && (

{[ c.floorArea ? locNum(c.floorArea, lang, { unit: "m²" }) : null, c.yearBuilt ? `${fr ? "constr." : "built"} ${c.yearBuilt}` : null, ] .filter(Boolean) .join(" · ")}

)}

{t("adjTime")} {signed(c.adjTime, lang)} · {t("adjArea")} {signed(c.adjArea, lang)} ·{" "} {t("adjAge")} {signed(c.adjAge, lang)}

{fmt(Math.round(c.adjustedPrice), lang)}

{t("weight")} {(c.weight * 100).toFixed(0)} %

{/* ================= alerte de valeur ================= */}
06 — {fr ? "Suivi" : "Watch"}

{t("leadTitle")}

); }