SPB Git forge

spb/uqo-imm1003

Public
7commits 1branches 0releases
189.5 MBsize
maindefault branch
7 h agolast push
JavaScript 68% CSS 32%
25.7 KB · 406 lines javascript
Raw Blame History
1// outils-imm1003.js — calculateurs du cours IMM1003 (Éléments d'évaluation immobilière)2// Chaque outil : { id, title, ref, desc, formula (HTML), inputs[], compute(v) → [{label, value, fmt, cls}] }3window.UQO_TOOLS = [4  {5    id: "rpv",6    title: "Ratio prêt/valeur et mise de fonds",7    ref: { label: "Formule 1.1 · séance 1", url: "/seance/01/#form-1-1" },8    desc: "Le prêteur retient la moindre du prix et de la valeur d’évaluation. Jusqu’à 80 % de RPV, le prêt est conventionnel ; au-delà, l’assurance prêt hypothécaire est obligatoire.",9    formula: "RPV = Prêt ÷ Valeur retenue × 100 · Valeur retenue = min(prix, évaluation)",10    inputs: [11      { key: "prix", label: "Prix d’achat accepté", default: 450000, unit: "$", step: 1000 },12      { key: "eval", label: "Valeur d’évaluation", default: 430000, unit: "$", step: 1000 },13      { key: "mise", label: "Mise de fonds prévue (en % du prix)", default: 20, unit: "%", step: 0.5 },14      { key: "rpvmax", label: "RPV maximal visé", default: 80, unit: "%", step: 1 },15    ],16    compute(v) {17      const retenue = Math.min(v.prix, v.eval);18      const pretMax = retenue * v.rpvmax / 100;19      const miseReq = v.prix - pretMax;20      const misePrev = v.prix * v.mise / 100;21      const pretPrev = v.prix - misePrev;22      const rpvPrev = pretPrev / retenue * 100;23      return [24        { label: "Valeur retenue par le prêteur (la moindre)", value: retenue },25        { label: `Prêt maximal à ${v.rpvmax} % de RPV`, value: pretMax },26        { label: "Mise de fonds requise (prix − prêt)", value: miseReq, cls: "main" },27        { label: "Mise de fonds prévue", value: misePrev },28        { label: "Écart à combler", value: miseReq - misePrev, cls: miseReq - misePrev > 0 ? "warn" : "good" },29        { label: "RPV effectif avec la mise prévue", value: rpvPrev, fmt: "pct1", cls: rpvPrev > 80 ? "warn" : "good" },30        { label: rpvPrev > 80 ? "Assurance prêt hypothécaire obligatoire (RPV > 80 %)" : "Prêt conventionnel (RPV ≤ 80 %)", value: rpvPrev > 80 ? "SCHL / Sagen / Canada Guaranty" : "aucune assurance requise", fmt: "text" },31      ];32    },33  },34  {35    id: "mutation",36    title: "Droits de mutation immobilière (barème 2026)",37    ref: { label: "Aide-mémoire · ratios", url: "/aide-memoire/" },38    desc: "Barème général par tranches, appliqué au plus élevé du prix payé et de la valeur au rôle pondérée. Les municipalités peuvent majorer les tranches au-delà de 500 000 $ (jusqu’à 3 %) ; Montréal a son propre barème.",39    formula: "0,5 % jusqu’à 62 900 $ · 1,0 % de 62 900 $ à 315 000 $ · 1,5 % au-delà (+ taux municipal facultatif au-delà de 500 000 $)",40    inputs: [41      { key: "base", label: "Base d’imposition (prix ou valeur au rôle pondérée, la plus élevée)", default: 477000, unit: "$", step: 1000 },42      { key: "t1", label: "Seuil 1", default: 62900, unit: "$", step: 100 },43      { key: "t2", label: "Seuil 2", default: 315000, unit: "$", step: 100 },44      { key: "muni", label: "Taux municipal majoré au-delà de 500 000 $ (0 = barème de base)", default: 0, unit: "%", step: 0.25, min: 0, max: 3 },45    ],46    compute(v) {47      const b = v.base;48      const tr1 = Math.min(b, v.t1) * 0.005;49      const tr2 = Math.max(0, Math.min(b, v.t2) - v.t1) * 0.01;50      let tr3, tr4 = 0;51      if (v.muni > 0 && b > 500000) {52        tr3 = Math.max(0, 500000 - v.t2) * 0.015;53        tr4 = (b - 500000) * v.muni / 100;54      } else {55        tr3 = Math.max(0, b - v.t2) * 0.015;56      }57      const total = tr1 + tr2 + tr3 + tr4;58      const out = [59        { label: `Tranche 1 — 0,5 % sur ${new Intl.NumberFormat("fr-CA").format(Math.min(b, v.t1))} $`, value: tr1, fmt: "money2" },60        { label: "Tranche 2 — 1,0 %", value: tr2, fmt: "money2" },61        { label: "Tranche 3 — 1,5 %", value: tr3, fmt: "money2" },62      ];63      if (tr4) out.push({ label: `Tranche municipale — ${v.muni} % au-delà de 500 000 $`, value: tr4, fmt: "money2" });64      out.push({ label: "Droits de mutation exigibles", value: total, fmt: "money2", cls: "main" });65      out.push({ label: "Taux effectif", value: total / b * 100, fmt: "pct" });66      return out;67    },68  },69  {70    id: "temporel",71    title: "Ajustement temporel d’un comparable",72    ref: { label: "Formule · séance 8", url: "/seance/08/" },73    desc: "Corrige le prix d’un comparable pour l’évolution du marché entre sa date de vente et la date d’évaluation. C’est un ajustement transactionnel, appliqué avant les ajustements physiques.",74    formula: "Ajustement = Prix × g<sub>mensuel</sub> × n<sub>mois</sub> (linéaire) ou Prix × (1 + g<sub>mensuel</sub>)<sup>n</sup> − Prix (composé)",75    inputs: [76      { key: "prix", label: "Prix de vente du comparable", default: 462000, unit: "$", step: 1000 },77      { key: "g", label: "Taux de variation du marché (annuel)", default: 6, unit: "%/an", step: 0.5 },78      { key: "mois", label: "Mois écoulés depuis la vente", default: 7, unit: "mois", step: 1, min: 0 },79    ],80    compute(v) {81      const gm = v.g / 100 / 12;82      const lin = v.prix * gm * v.mois;83      const comp = v.prix * Math.pow(1 + gm, v.mois) - v.prix;84      return [85        { label: "Taux mensuel équivalent", value: gm * 100, fmt: "pct3" },86        { label: "Ajustement linéaire (méthode du cours)", value: lin, cls: "main" },87        { label: "Prix ajusté (linéaire)", value: v.prix + lin },88        { label: "Ajustement composé (variante)", value: comp },89        { label: "Prix ajusté (composé)", value: v.prix + comp },90        { label: "Poids de l’ajustement dans le prix", value: lin / v.prix * 100, fmt: "pct", cls: Math.abs(lin / v.prix) > 0.1 ? "warn" : "" },91      ];92    },93  },94  {95    id: "grille",96    title: "Grille de comparaison — trois comparables",97    ref: { label: "Séance 9 · ajustements et réconciliation", url: "/seance/09/" },98    desc: "Saisissez le prix et les ajustements (du comparable vers le sujet : supérieur → négatif, inférieur → positif). L’outil calcule les prix ajustés, les ajustements net et brut, vérifie les seuils (net ≤ 15 %, brut ≤ 25 %) et propose une réconciliation pondérée par la fiabilité.",99    formula: "Prix ajusté = Prix × (1 + temps) + Σ ajustements $ · Net = Σ ajustements / prix · Brut = Σ |ajustements| / prix",100    inputs: [101      { group: "Comparable 1" },102      { key: "p1", label: "Prix", default: 445000, unit: "$", step: 1000 },103      { key: "t1", label: "Ajustement temporel", default: 2, unit: "%", step: 0.5 },104      { key: "a1", label: "Localisation", default: 0, unit: "$", step: 1000 },105      { key: "b1", label: "Superficie / physique", default: -12000, unit: "$", step: 1000 },106      { key: "c1", label: "Garage, sous-sol, extras", default: 8000, unit: "$", step: 1000 },107      { group: "Comparable 2" },108      { key: "p2", label: "Prix", default: 472000, unit: "$", step: 1000 },109      { key: "t2", label: "Ajustement temporel", default: 1, unit: "%", step: 0.5 },110      { key: "a2", label: "Localisation", default: -15000, unit: "$", step: 1000 },111      { key: "b2", label: "Superficie / physique", default: -8000, unit: "$", step: 1000 },112      { key: "c2", label: "Garage, sous-sol, extras", default: -10000, unit: "$", step: 1000 },113      { group: "Comparable 3" },114      { key: "p3", label: "Prix", default: 418000, unit: "$", step: 1000 },115      { key: "t3", label: "Ajustement temporel", default: 3.5, unit: "%", step: 0.5 },116      { key: "a3", label: "Localisation", default: 10000, unit: "$", step: 1000 },117      { key: "b3", label: "Superficie / physique", default: 15000, unit: "$", step: 1000 },118      { key: "c3", label: "Garage, sous-sol, extras", default: 12000, unit: "$", step: 1000 },119    ],120    compute(v) {121      const rows = [];122      const res = [];123      let wsum = 0, vsum = 0;124      for (let i = 1; i <= 3; i++) {125        const p = v["p" + i];126        const pt = p * (1 + v["t" + i] / 100);127        const adj = [v["a" + i], v["b" + i], v["c" + i]];128        const net = adj.reduce((a, b) => a + b, 0) + (pt - p);129        const brut = adj.reduce((a, b) => a + Math.abs(b), 0) + Math.abs(pt - p);130        const pa = pt + adj.reduce((a, b) => a + b, 0);131        const netPct = net / p * 100, brutPct = brut / p * 100;132        const fiable = Math.abs(netPct) <= 15 && brutPct <= 25;133        const w = fiable ? 1 / Math.max(brutPct, 1) : 0.25 / Math.max(brutPct, 1);134        wsum += w; vsum += w * pa;135        rows.push([`C${i}`, p, pa, netPct, brutPct, fiable ? "✓" : "⚠ hors seuil"]);136      }137      const concl = vsum / wsum;138      const arrondi = Math.round(concl / 5000) * 5000;139      const vals = rows.map((r) => r[2]);140      res.push({ table: { head: ["Comp.", "Prix", "Prix ajusté", "Net", "Brut", "Seuils"], rows, fmt: [null, "money", "money", "pct1", "pct1", "text"] } });141      res.push({ label: "Fourchette des indications", value: `${new Intl.NumberFormat("fr-CA").format(Math.round(Math.min(...vals)))} $ – ${new Intl.NumberFormat("fr-CA").format(Math.round(Math.max(...vals)))} $`, fmt: "text" });142      res.push({ label: "Écart de la fourchette", value: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) * 100, fmt: "pct1", cls: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) > 0.15 ? "warn" : "good" });143      res.push({ label: "Réconciliation pondérée par la fiabilité (poids ∝ 1/brut)", value: concl, cls: "main" });144      res.push({ label: "Conclusion arrondie (au 5 000 $)", value: arrondi });145      return res;146    },147  },148  {149    id: "rne",150    title: "Cascade des revenus : RBP → RBE → RNE → valeur",151    ref: { label: "Formules 11.x · séance 11", url: "/seance/11/" },152    desc: "Reconstruisez le revenu net d’exploitation d’un immeuble locatif puis capitalisez-le. Le service de la dette, l’amortissement et l’impôt sont exclus ; la gestion s’impute même en autogestion ; la réserve pour remplacement fait partie des dépenses.",153    formula: "RBE = RBP − inoccupation − mauvaises créances + autres revenus · RNE = RBE − dépenses · V = RNE ÷ TGA",154    inputs: [155      { key: "n", label: "Nombre de logements", default: 6, unit: "", step: 1, min: 1 },156      { key: "loyer", label: "Loyer mensuel moyen du marché", default: 1050, unit: "$/mois", step: 25 },157      { key: "autres", label: "Autres revenus annuels (stationnement, buanderie)", default: 2400, unit: "$/an", step: 100 },158      { key: "inocc", label: "Inoccupation et mauvaises créances", default: 4, unit: "% du RBP", step: 0.5 },159      { group: "Dépenses d’exploitation annuelles" },160      { key: "taxes", label: "Taxes municipales et scolaires", default: 9800, unit: "$", step: 100 },161      { key: "assur", label: "Assurances", default: 3600, unit: "$", step: 100 },162      { key: "entretien", label: "Entretien et réparations", default: 5200, unit: "$", step: 100 },163      { key: "energie", label: "Énergie (espaces communs) et déneigement", default: 3100, unit: "$", step: 100 },164      { key: "gestion", label: "Gestion", default: 5, unit: "% du RBE", step: 0.5 },165      { key: "reserve", label: "Réserve pour remplacement", default: 3, unit: "% du RBE", step: 0.5 },166      { group: "Capitalisation" },167      { key: "tga", label: "Taux global d’actualisation (TGA)", default: 5.5, unit: "%", step: 0.1 },168    ],169    compute(v) {170      const rbp = v.n * v.loyer * 12;171      const pertes = rbp * v.inocc / 100;172      const rbe = rbp - pertes + v.autres;173      const gestion = rbe * v.gestion / 100;174      const reserve = rbe * v.reserve / 100;175      const dep = v.taxes + v.assur + v.entretien + v.energie + gestion + reserve;176      const rne = rbe - dep;177      const ratio = dep / rbe * 100;178      const val = rne / (v.tga / 100);179      return [180        { label: "Revenu brut potentiel (RBP)", value: rbp },181        { label: `− Inoccupation et mauvaises créances (${v.inocc} %)`, value: -pertes },182        { label: "+ Autres revenus", value: v.autres },183        { label: "= Revenu brut effectif (RBE)", value: rbe, cls: "good" },184        { label: "Gestion", value: gestion },185        { label: "Réserve pour remplacement", value: reserve },186        { label: "− Dépenses d’exploitation totales", value: -dep },187        { label: "Ratio dépenses / RBE (repère 35–45 % pour 6–24 logements)", value: ratio, fmt: "pct1", cls: ratio < 30 || ratio > 50 ? "warn" : "good" },188        { label: "= Revenu net d’exploitation (RNE)", value: rne, cls: "good" },189        { label: `Valeur par capitalisation directe à ${v.tga} %`, value: val, cls: "main" },190        { label: "Valeur par logement", value: val / v.n },191        { label: "MRB implicite (valeur / RBE)", value: val / rbe, fmt: "num" },192      ];193    },194  },195  {196    id: "tga",197    title: "TGA : extraction, bande d’investissement et sommation",198    ref: { label: "Formules 12.x · séance 12", url: "/seance/12/" },199    desc: "Trois façons d’estimer le taux global d’actualisation : l’extraction sur une vente comparable (méthode reine), la bande d’investissement hypothèque–équité et la construction par sommation. Comparez-les.",200    formula: "TGA = RNE ÷ Prix · TGA = M·R<sub>m</sub> + (1 − M)·R<sub>e</sub> · R<sub>m</sub> = 12 × (i/12) / (1 − (1 + i/12)<sup>−n</sup>)",201    inputs: [202      { group: "Extraction du marché" },203      { key: "rne", label: "RNE du comparable", value: 0, default: 46800, unit: "$", step: 100 },204      { key: "prix", label: "Prix de vente du comparable", default: 851000, unit: "$", step: 1000 },205      { group: "Bande d’investissement" },206      { key: "m", label: "Ratio prêt/valeur (M)", default: 75, unit: "%", step: 1 },207      { key: "i", label: "Taux d’intérêt hypothécaire", default: 5.5, unit: "%", step: 0.05 },208      { key: "amort", label: "Amortissement", default: 25, unit: "ans", step: 1 },209      { key: "re", label: "Rendement exigé sur l’équité (R<sub>e</sub>)", default: 8, unit: "%", step: 0.25 },210      { group: "Sommation" },211      { key: "rf", label: "Taux sans risque (obligations 10 ans)", default: 3.2, unit: "%", step: 0.05 },212      { key: "prisque", label: "Prime de risque immobilier", default: 2.0, unit: "%", step: 0.1 },213      { key: "pliq", label: "Prime de non-liquidité", default: 1.0, unit: "%", step: 0.1 },214      { key: "pgest", label: "Prime de gestion", default: 0.5, unit: "%", step: 0.1 },215      { key: "g", label: "− Croissance anticipée des revenus", default: 1.5, unit: "%", step: 0.1 },216    ],217    compute(v) {218      const ext = v.rne / v.prix * 100;219      const im = v.i / 100 / 12;220      const n = v.amort * 12;221      const rm = 12 * im / (1 - Math.pow(1 + im, -n)) * 100;222      const bande = v.m / 100 * rm + (1 - v.m / 100) * v.re;223      const somm = v.rf + v.prisque + v.pliq + v.pgest - v.g;224      const vals = [ext, bande, somm];225      return [226        { label: "TGA extrait du marché (RNE ÷ prix)", value: ext, fmt: "pct", cls: "main" },227        { label: "Constante hypothécaire annuelle R<sub>m</sub>", value: rm, fmt: "pct" },228        { label: "TGA par bande d’investissement", value: bande, fmt: "pct" },229        { label: "TGA par sommation (taux construit)", value: somm, fmt: "pct" },230        { label: "Écart max. entre les trois méthodes", value: Math.max(...vals) - Math.min(...vals), fmt: "pct", cls: Math.max(...vals) - Math.min(...vals) > 1 ? "warn" : "good" },231        { label: "Multiplicateur de revenu net implicite (1/TGA extrait)", value: 100 / ext, fmt: "num" },232      ];233    },234  },235  {236    id: "dcf",237    title: "Actualisation des flux monétaires (DCF)",238    ref: { label: "Formule DCF · séance 12", url: "/seance/12/" },239    desc: "Valeur actuelle des RNE sur la période de détention plus la valeur de réversion (RNE de l’année n+1 capitalisé au TGA de sortie, moins les frais de vente). Observez le poids de la réversion et la sensibilité au taux.",240    formula: "V<sub>0</sub> = Σ RNE<sub>t</sub>/(1+r)<sup>t</sup> + V<sub>n</sub>/(1+r)<sup>n</sup> · V<sub>n</sub> = RNE<sub>n+1</sub> ÷ TGA<sub>sortie</sub> − frais de vente",241    inputs: [242      { key: "rne1", label: "RNE de l’année 1", default: 128000, unit: "$", step: 1000 },243      { key: "g", label: "Croissance annuelle du RNE", default: 2.5, unit: "%", step: 0.25 },244      { key: "n", label: "Période de détention", default: 5, unit: "ans", step: 1, min: 1, max: 15 },245      { key: "r", label: "Taux d’actualisation (r)", default: 7.5, unit: "%", step: 0.25 },246      { key: "tgaout", label: "TGA de sortie", default: 5.75, unit: "%", step: 0.25 },247      { key: "frais", label: "Frais de vente à la réversion", default: 3, unit: "% de la valeur brute", step: 0.5 },248    ],249    compute(v) {250      const r = v.r / 100, g = v.g / 100;251      let pv = 0;252      const rows = [];253      let rne = v.rne1;254      for (let t = 1; t <= v.n; t++) {255        const f = 1 / Math.pow(1 + r, t);256        pv += rne * f;257        rows.push([`Année ${t}`, rne, f, rne * f]);258        rne *= 1 + g;259      }260      const rneN1 = rne; // RNE année n+1261      const vBrute = rneN1 / (v.tgaout / 100);262      const vNette = vBrute * (1 - v.frais / 100);263      const pvRev = vNette / Math.pow(1 + r, v.n);264      const v0 = pv + pvRev;265      const capDirecte = v.rne1 / ((v.r - v.g) / 100);266      // sensibilité ±0,5 point sur r267      const sens = (dr) => { let s = 0, x = v.rne1; for (let t = 1; t <= v.n; t++) { s += x / Math.pow(1 + r + dr, t); x *= 1 + g; } return s + vNette / Math.pow(1 + r + dr, v.n); };268      return [269        { table: { head: ["Année", "RNE", "Facteur", "Valeur actuelle"], rows, fmt: [null, "money", "num4", "money"] } },270        { label: "VA des RNE de la période", value: pv },271        { label: `RNE de l’année ${v.n + 1} (base de la réversion)`, value: rneN1 },272        { label: "Valeur de réversion brute", value: vBrute },273        { label: "Valeur de réversion nette de frais", value: vNette },274        { label: "VA de la réversion", value: pvRev },275        { label: "Poids de la réversion dans la valeur", value: pvRev / v0 * 100, fmt: "pct1", cls: pvRev / v0 > 0.8 ? "warn" : "" },276        { label: "Valeur par DCF", value: v0, cls: "main" },277        { label: "Vérification : capitalisation directe au TGA ≈ r − g", value: capDirecte },278        { label: "Sensibilité : r + 0,5 point", value: sens(0.005) },279        { label: "Sensibilité : r − 0,5 point", value: sens(-0.005) },280      ];281    },282  },283  {284    id: "cout",285    title: "Méthode du coût simplifiée (âge/vie modifiée)",286    ref: { label: "Formules 10.x · séance 10", url: "/seance/10/" },287    desc: "V = coût neuf − dépréciation + terrain. L’entretien différé (curable) est retiré de la base, puis le ratio âge effectif / vie utile s’applique au reste ; les dépréciations fonctionnelle et économique s’ajoutent.",288    formula: "V = C<sub>neuf</sub> − D<sub>totale</sub> + V<sub>terrain</sub> · D<sub>phys. inc.</sub> = (C<sub>neuf</sub> − curable) × âge effectif / vie utile",289    inputs: [290      { key: "sup", label: "Superficie habitable", default: 1650, unit: "pi²", step: 10 },291      { key: "cu", label: "Coût direct unitaire", default: 165, unit: "$/pi²", step: 5 },292      { key: "ind", label: "Coûts indirects", default: 12, unit: "% des directs", step: 1 },293      { key: "profit", label: "Profit de l’entrepreneur", default: 10, unit: "% des directs", step: 1 },294      { key: "curable", label: "Entretien différé (curable)", default: 9500, unit: "$", step: 500 },295      { key: "age", label: "Âge effectif", default: 18, unit: "ans", step: 1, min: 0 },296      { key: "vie", label: "Vie utile économique", default: 60, unit: "ans", step: 5, min: 1 },297      { key: "fonc", label: "Dépréciation fonctionnelle", default: 6000, unit: "$", step: 500 },298      { key: "eco", label: "Dépréciation économique (externe)", default: 0, unit: "$", step: 500 },299      { key: "terrain", label: "Valeur du terrain (vacant, utilisation optimale)", default: 145000, unit: "$", step: 1000 },300    ],301    compute(v) {302      const directs = v.sup * v.cu;303      const indirects = directs * v.ind / 100;304      const profit = directs * v.profit / 100;305      const cneuf = directs + indirects + profit;306      const base = cneuf - v.curable;307      const inc = base * Math.min(1, v.age / v.vie);308      const dtot = v.curable + inc + v.fonc + v.eco;309      const val = cneuf - dtot + v.terrain;310      return [311        { label: "Coûts directs", value: directs },312        { label: "Coûts indirects", value: indirects },313        { label: "Profit de l’entrepreneur", value: profit },314        { label: "Coût à neuf", value: cneuf, cls: "good" },315        { label: "Dépréciation physique curable (entretien différé)", value: v.curable },316        { label: `Dépréciation physique incurable (${v.age}/${v.vie} de la base résiduelle)`, value: inc },317        { label: "Dépréciation fonctionnelle + économique", value: v.fonc + v.eco },318        { label: "Dépréciation totale", value: dtot },319        { label: "Taux global de dépréciation", value: dtot / cneuf * 100, fmt: "pct1", cls: dtot >= cneuf ? "warn" : "" },320        { label: "Valeur dépréciée des améliorations", value: cneuf - dtot },321        { label: "+ Valeur du terrain", value: v.terrain },322        { label: "Valeur par la méthode du coût", value: val, cls: "main" },323        { label: "Arrondie au 5 000 $", value: Math.round(val / 5000) * 5000 },324      ];325    },326  },327  {328    id: "reconciliation",329    title: "Réconciliation pondérée des trois méthodes",330    ref: { label: "Séance 14 · réconciliation", url: "/seance/14/" },331    desc: "La conclusion n’est jamais une moyenne : chaque indication est pondérée selon la pertinence de la méthode, la qualité des données et le comportement des acheteurs types. Les poids doivent totaliser 100 %.",332    formula: "V<sub>conclue</sub> = Σ w<sub>i</sub> × V<sub>i</sub>, avec Σ w<sub>i</sub> = 1",333    inputs: [334      { key: "vc", label: "Indication — comparaison", default: 1580000, unit: "$", step: 5000 },335      { key: "wc", label: "Poids — comparaison", default: 40, unit: "%", step: 5 },336      { key: "vk", label: "Indication — coût", default: 1610000, unit: "$", step: 5000 },337      { key: "wk", label: "Poids — coût", default: 15, unit: "%", step: 5 },338      { key: "vr", label: "Indication — revenu", default: 1580000, unit: "$", step: 5000 },339      { key: "wr", label: "Poids — revenu", default: 45, unit: "%", step: 5 },340      { key: "arr", label: "Arrondi", default: 5000, unit: "$", step: 1000 },341    ],342    compute(v) {343      const w = v.wc + v.wk + v.wr;344      const val = (v.vc * v.wc + v.vk * v.wk + v.vr * v.wr) / 100;345      const moy = (v.vc + v.vk + v.vr) / 3;346      const vals = [v.vc, v.vk, v.vr];347      return [348        { label: "Somme des poids", value: w, fmt: "pct1", cls: Math.abs(w - 100) > 0.01 ? "warn" : "good" },349        { label: "Valeur pondérée", value: val * 100 / w, cls: "main" },350        { label: `Conclusion arrondie (${new Intl.NumberFormat("fr-CA").format(v.arr)} $)`, value: Math.round(val * 100 / w / v.arr) * v.arr },351        { label: "Moyenne simple (à titre indicatif seulement — jamais la conclusion)", value: moy },352        { label: "Dispersion des indications (max − min) / min", value: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) * 100, fmt: "pct1", cls: (Math.max(...vals) - Math.min(...vals)) / Math.min(...vals) > 0.1 ? "warn" : "good" },353      ];354    },355  },356  {357    id: "tvm",358    title: "Valeur temps de l’argent",359    ref: { label: "Formules VF / VA · séance 12", url: "/seance/12/" },360    desc: "Un dollar reçu aujourd’hui vaut plus qu’un dollar reçu demain. Capitalisation d’une somme, actualisation d’un montant futur et constante hypothécaire.",361    formula: "FV = PV × (1 + r)<sup>n</sup> · PV = FV ÷ (1 + r)<sup>n</sup>",362    inputs: [363      { key: "pv", label: "Montant présent (PV)", default: 100000, unit: "$", step: 1000 },364      { key: "fv", label: "Montant futur à actualiser (FV)", default: 150000, unit: "$", step: 1000 },365      { key: "r", label: "Taux annuel", default: 5, unit: "%", step: 0.25 },366      { key: "n", label: "Nombre d’années", default: 10, unit: "ans", step: 1, min: 0 },367    ],368    compute(v) {369      const r = v.r / 100;370      const fv = v.pv * Math.pow(1 + r, v.n);371      const pv = v.fv / Math.pow(1 + r, v.n);372      const doubling = Math.log(2) / Math.log(1 + r);373      return [374        { label: `Valeur future de ${new Intl.NumberFormat("fr-CA").format(v.pv)} $ dans ${v.n} ans`, value: fv, cls: "main" },375        { label: `Valeur présente de ${new Intl.NumberFormat("fr-CA").format(v.fv)} $ reçus dans ${v.n} ans`, value: pv },376        { label: "Facteur de capitalisation (1 + r)ⁿ", value: Math.pow(1 + r, v.n), fmt: "num4" },377        { label: "Facteur d’actualisation 1/(1 + r)ⁿ", value: 1 / Math.pow(1 + r, v.n), fmt: "num4" },378        { label: "Temps de doublement au taux r", value: doubling, fmt: "num" },379      ];380    },381  },382  {383    id: "unites",384    title: "Conversions d’unités et prix unitaires",385    ref: { label: "Aide-mémoire · conversions", url: "/aide-memoire/" },386    desc: "Pi² ↔ m², acres ↔ hectares, et prix unitaires cohérents entre les systèmes.",387    formula: "1 m² ≈ 10,764 pi² · 1 acre ≈ 43 560 pi² ≈ 4 047 m² · 1 ha = 10 000 m²",388    inputs: [389      { key: "pi2", label: "Superficie", default: 1650, unit: "pi²", step: 10 },390      { key: "prix", label: "Prix total", default: 477000, unit: "$", step: 1000 },391      { key: "acres", label: "Terrain", default: 0.5, unit: "acre", step: 0.05 },392    ],393    compute(v) {394      const m2 = v.pi2 / 10.7639;395      return [396        { label: "Superficie en m²", value: m2, fmt: "num" },397        { label: "Prix au pi²", value: v.prix / v.pi2, fmt: "money2", cls: "main" },398        { label: "Prix au m²", value: v.prix / m2, fmt: "money2" },399        { label: "Terrain en pi²", value: v.acres * 43560, fmt: "int" },400        { label: "Terrain en m²", value: v.acres * 4046.86, fmt: "int" },401        { label: "Terrain en hectares", value: v.acres * 0.404686, fmt: "num4" },402      ];403    },404  },405];406