// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
/**
* Pro forma interactif de l'investisseur — « avec les frais et tout le kit ».
* Loyer moyen implicite qui justifie la valeur, cascade revenus → RNE,
* financement hypothécaire canadien, cashflow par porte, DSCR, droits de
* mutation et liquidités requises. Chaque hypothèse est ajustable en direct.
*/
"use client";
import { useState } from "react";
import { buildProforma, DEFAULT_PARAMS, tgaReference } from "@/lib/proforma";
import { useLang } from "./LangContext";
const fmt = (v: number, lang: string, frac = 0) =>
new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", {
style: "currency",
currency: "CAD",
maximumFractionDigits: frac,
}).format(v);
function Slider({
label, value, min, max, step, unit, onChange,
}: {
label: string; value: number; min: number; max: number; step: number;
unit: string; onChange: (v: number) => void;
}) {
return (
{label}
{value.toLocaleString("fr-CA")} {unit}
onChange(Number(e.target.value))}
className="h-2 w-full cursor-pointer appearance-none rounded-full border-[1.5px] border-ink bg-surface-2 accent-[var(--green)]"
/>
);
}
function Row({ k, v, strong, neg }: { k: string; v: string; strong?: boolean; neg?: boolean }) {
return (
{k}
{v}
);
}
export default function Proforma({
valeur, doors, valeurRole, municipalite,
}: {
valeur: number; doors: number; valeurRole: number | null; municipalite: string | null;
}) {
const { lang } = useLang();
const fr = lang === "fr";
const [taux, setTaux] = useState(DEFAULT_PARAMS.tauxHypoPct);
const [mise, setMise] = useState(DEFAULT_PARAMS.miseDeFondsPct);
const [amort, setAmort] = useState(DEFAULT_PARAMS.amortAns);
const [tga, setTga] = useState(tgaReference(doors));
const [appr, setAppr] = useState(DEFAULT_PARAMS.appreciationPct);
const pf = buildProforma(valeur, doors, valeurRole, municipalite, {
tauxHypoPct: taux, miseDeFondsPct: mise, amortAns: amort, tgaPct: tga,
appreciationPct: appr,
});
const DEP_FR: Record = {
taxesMunicipales: fr ? "Taxes municipales (≈1,1 % du rôle)" : "Municipal taxes (≈1.1% of roll)",
taxeScolaire: fr ? "Taxe scolaire (≈0,1 % du rôle)" : "School tax (≈0.1% of roll)",
assurances: fr ? "Assurances" : "Insurance",
entretien: fr ? "Entretien et réparations (5 % RB)" : "Maintenance & repairs (5% GI)",
gestion: fr ? "Gestion (4 % RB)" : "Management (4% GI)",
deneigementPelouse: fr ? "Déneigement et pelouse" : "Snow & lawn",
energieCommuns: fr ? "Énergie des espaces communs" : "Common-area energy",
reserveRemplacement: fr ? "Réserve de remplacement" : "Replacement reserve",
};
return (
{fr ? "Approche revenu — interactif" : "Income approach — interactive"}
{fr ? "Pro forma de l'investisseur" : "Investor pro forma"}
{fr
? `Quelle structure de revenus la valeur estimée suppose-t-elle ? Au TGA choisi, ce ${doors <= 6 ? "plex" : "immeuble"} doit générer un loyer moyen de `
: `What income structure does the estimated value imply? At the chosen cap rate, this building must generate an average rent of `}
{fmt(pf.loyerMoyenMensuel, lang)}
{fr ? " par porte par mois. Ajustez les hypothèses — tout recalcule." : " per door per month. Adjust the assumptions — everything recomputes."}
{/* hypothèses ajustables */}
{/* état des résultats */}
{fr ? "État des résultats normalisé (annuel)" : "Normalized operating statement (annual)"}
|
{pf.depenses.map((d) => (
|
))}
TGA {fr ? "implicite" : "implied"} : {pf.tgaImplicitePct.toFixed(2)} %
MRB : × {pf.mrb.toFixed(1)}
{fr ? "RNE/porte" : "NOI/door"} : {fmt(pf.rne / doors, lang)}
{/* financement + acquisition */}
{fr ? "Financement" : "Financing"}
= 1.2 ? "pill-conf-A" : pf.dscr >= 1.0 ? "pill-conf-C" : "pill-conf-D"}`}>
DSCR {pf.dscr.toFixed(2)}
= 0 ? "pill-conf-B" : "pill-conf-D"}`}>
{fmt(pf.cashflowMensuelParPorte, lang)} / {fr ? "porte / mois" : "door / mo"}
= 4 ? "pill-conf-A" : pf.cashOnCashPct >= 0 ? "pill-conf-C" : "pill-conf-D"}`}>
{fr ? "Rend. liquidités" : "Cash-on-cash"} {pf.cashOnCashPct.toFixed(1)} %
{fr ? "Liquidités requises — tout le kit" : "Cash required — the whole kit"}
{fr ? "Total à prévoir" : "Total to plan"}
{fmt(pf.liquiditesRequises, lang)}
{/* projection 5 ans + marges de sécurité */}
{fr ? `Création de richesse — 5 ans (appréciation ${pf.params.appreciationPct} %/an)` : `Wealth creation — 5 years (${pf.params.appreciationPct}%/yr appreciation)`}
= 1.8 ? "pill-conf-A" : pf.multipleLiquidites5Ans >= 1.2 ? "pill-conf-B" : "pill-conf-C"}`}>
{fr ? "Multiple sur liquidités à 5 ans" : "5-yr equity multiple"} : × {pf.multipleLiquidites5Ans.toFixed(2)}
{fr ? "Marges de sécurité et sensibilité" : "Safety margins & sensitivity"}
{fr ? "Cashflow selon le taux" : "Cashflow by rate"}
{pf.sensibiliteTaux.map((sc) => (
= 0 ? "pill-conf-B" : "pill-conf-D"}`}>
{sc.tauxPct.toFixed(2)} % → {fmt(sc.cashflowAnnuel, lang)}/{fr ? "an" : "yr"} · DSCR {sc.dscr.toFixed(2)}
))}
{fr
? "Pro forma indicatif à hypothèses normalisées (loyer implicite dérivé de la valeur estimée et du TGA — pas des baux réels). Taxes approximées depuis la valeur au rôle. Ne constitue pas un conseil financier."
: "Indicative pro forma with normalized assumptions (implied rent derived from estimated value and cap rate — not actual leases). Taxes approximated from assessed value. Not financial advice."}
);
}