// Auteur : Simon-Pierre Boucher — contact@spboucher.ai "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}
{t("range")}
{t("assessed")}
{fmt(u.valeurRole, lang)}
{fr ? "Écart vs rôle" : "vs assessment"}
{r.estimate >= u.valeurRole ? "+" : ""} {((r.estimate / u.valeurRole - 1) * 100).toFixed(0)} %
{t("disclaimer")}
{fr ? "Bâtiment" : "Building"}
{fr ? "Terrain — à l'échelle" : "Lot — to scale"}
{fr ? "Valeur au rôle 2026 — composition" : "2026 assessed value — split"}
{fmt(u.valeurRole, lang)}
{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} %` : ""}
{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)})`}
{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."}
{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 }) : "—"}
{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"}
)}{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. > )}
| # | {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)} % |