// Auteur : Simon-Pierre Boucher — contact@spboucher.ai "use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import SearchBox, { type Suggestion } from "@/components/SearchBox"; import { CountUp, locNum, Sparkline, ValueSplit } from "@/components/MetricViz"; import { useLang } from "@/components/LangContext"; import type { PortfolioResult } from "@/lib/estimator"; import type { TKey } from "@/lib/i18n"; const KEY = "vp-parc"; interface Saved { id: string; adresse: string; municipalite: string; } 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 CONF: Record = { A: "pill-conf-A", B: "pill-conf-B", C: "pill-conf-C", D: "pill-conf-D", }; export default function ParcPage() { const { lang, t } = useLang(); const fr = lang === "fr"; const [list, setList] = useState([]); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [hydrated, setHydrated] = useState(false); useEffect(() => { const id = setTimeout(() => { try { const raw = window.localStorage.getItem(KEY); if (raw) setList(JSON.parse(raw) as Saved[]); } catch {} setHydrated(true); }, 0); return () => clearTimeout(id); }, []); const persist = (l: Saved[]) => { setList(l); window.localStorage.setItem(KEY, JSON.stringify(l)); setData(null); }; const add = (s: Suggestion) => { if (list.some((x) => x.id === s.id) || list.length >= 40) return; persist([ ...list, { id: s.id, adresse: `${s.adresse ?? ""}${s.apt ? ` app. ${s.apt}` : ""}`, municipalite: s.municipalite ?? "", }, ]); }; const evaluate = async () => { setLoading(true); try { const res = await fetch("/api/portfolio", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: list.map((x) => x.id) }), }); if (res.ok) setData((await res.json()) as PortfolioResult); } finally { setLoading(false); } }; const a = data?.aggregates; return (
{/* héros */}
{fr ? "Investisseurs · propriétaires multiples" : "Investors · multi-owners"}

{fr ? ( <> La valeur de votre parc immobilier,
au complet. ) : ( <> Your property portfolio,
fully valued. )}

{fr ? "Ajoutez chacune de vos adresses. Vrai-Prix évalue chaque propriété, agrège votre parc — valeur totale, fourchette, croissance 2021→2026 — et produit un rapport PDF consolidé." : "Add each of your addresses. Vrai-Prix values every property, aggregates your portfolio — total value, range, 2021→2026 growth — and produces a consolidated PDF report."}

{/* constructeur de parc */}

{fr ? `Ajouter une propriété (${list.length}/40)` : `Add a property (${list.length}/40)`}

{hydrated && list.length > 0 && (
{list.map((p, i) => ( {i + 1} {p.adresse} · {p.municipalite} ))}
)}
{list.length > 0 && ( )}
{/* résultats */} {data && a && (
{/* total */}
{fr ? "Valeur totale estimée du parc" : "Total estimated portfolio value"}

new Intl.NumberFormat(fr ? "fr-CA" : "en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0, }).format(Math.round(v / 100) * 100) } />

{fr ? "Fourchette cumulée" : "Cumulative range"}

{fmt(a.totalLow, lang)} {fmt(a.totalHigh, lang)}

{fr ? "Confiance pondérée" : "Weighted confidence"}

{a.confidenceLevel} · {a.confidencePct} %

{fr ? "Évaluation municipale" : "Municipal assessment"}

{fmt(a.totalRole, lang)}

i.unit!.id).join(",")}`} className="btn btn-primary mt-6" download > {fr ? `Télécharger le rapport de parc (${1 + data.items.length} pages PDF)` : `Download the portfolio report (${1 + data.items.length} PDF pages)`}{" "} ↓
{/* tuiles registre */}
{fr ? "Votre parc, en chiffres du registre" : "Your portfolio, in registry numbers"}
{( [ [fr ? "Propriétés" : "Properties", String(a.count), true], [fr ? "Écart vs rôle" : "vs assessment", a.ecartRolePct != null ? `${a.ecartRolePct >= 0 ? "+" : ""}${a.ecartRolePct.toFixed(0)} %` : "—", false], [fr ? "Croissance 2021→2026" : "Growth 2021→2026", a.growthPct != null ? `${a.growthPct >= 0 ? "+" : ""}${a.growthPct.toFixed(0)} %` : "—", true], [fr ? "Logements" : "Dwellings", String(a.totalDwellings), false], [fr ? "Aire habitable" : "Living area", `${a.totalFloorArea.toLocaleString(fr ? "fr-CA" : "en-CA")} m²`, false], [fr ? "Terrain total" : "Total land", `${a.totalLandArea.toLocaleString(fr ? "fr-CA" : "en-CA")} m²`, false], [fr ? "Année moyenne" : "Avg. year built", a.avgYearBuilt ? String(a.avgYearBuilt) : "—", false], [fr ? "Municipalités" : "Municipalities", String(a.municipalities.length), false], ] as [string, string, boolean][] ).map(([k, v, hero]) => (

{v}

{k}

))}
{/* historique du parc + répartitions */}

{fr ? "Valeur du parc par année" : "Portfolio value by year"}

{a.history.map((h) => { const max = Math.max(...a.history.map((x) => x.total), 1); return (
{h.year} {fmt(h.total, lang)} {h.nCovered < a.count && ( ({h.nCovered}/{a.count}) )}
); })}

{fr ? "Répartition" : "Breakdown"}

{a.municipalities.slice(0, 4).map((m) => (
{m.name} {m.count} · {fmt(m.total, lang)}
))} {a.types.map((tp) => (
{t(tp.type as TKey)} {tp.count} · {fmt(tp.total, lang)}
))}
{/* cartes propriétés */}
{fr ? "Chaque propriété du parc" : "Every property in the portfolio"}
{data.items.map((it, i) => { const u = it.unit!; const r = it.result; return (

{String(i + 1).padStart(2, "0")}

{u.adresse}

{u.municipalite} · {u.anneeConstruction ?? "—"} ·{" "} {u.aireEtagesM2 ? locNum(u.aireEtagesM2, lang, { unit: "m²" }) : "—"}

{r.confidenceLevel}

{fmt(r.estimate, lang)}

{fr ? "rôle" : "roll"} {fmt(u.valeurRole, lang)}

); })}
)}
); }