TypeScript 90.1%
JavaScript 3.2%
Python 3.2%
CSS 1.8%
HTML 1.8%
1// UQO Éval — mode étudiant : l'analyse pédagogique complète d'une estimation.2/**3 * Décompose le résultat du moteur (engine.ts) SANS inventer de calcul :4 * chaque chiffre affiché est recomposé à partir des mêmes règles que le5 * moteur (sélection des comparables, gaussiennes de pondération, ajustements,6 * combinaison 65/35, score de confiance) appliquées aux données du résultat.7 */8"use client";9import { useLang } from "./LangContext";10import type { EstimateResult } from "@/lib/engine";1112const money = (v: number, lang: string) =>13 new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", {14 style: "currency",15 currency: "CAD",16 maximumFractionDigits: 0,17 }).format(Math.round(v));1819/** Reconstruit la décomposition du score de confiance (mêmes règles que engine.ts). */20function confidenceParts(r: EstimateResult) {21 const base = 35;22 const comps = Math.min(25, r.comps.length * 2.5);23 const disp =24 r.compsDispersionPct != null ? Math.max(0, 20 - r.compsDispersionPct) : 0;25 const model = r.modelEstimate != null ? 15 : 0;26 const widthPct = r.estimate > 0 ? ((r.high - r.low) / r.estimate) * 100 : 100;27 const width = -Math.max(0, (widthPct - 30) / 3);28 return { base, comps, disp, model, width, widthPct };29}3031function Formula({ children }: { children: React.ReactNode }) {32 return (33 <p className="vp-mono mt-2 overflow-x-auto whitespace-nowrap border-l-2 border-accent bg-surface px-3 py-2 text-[11.5px] text-ink">34 {children}35 </p>36 );37}3839function Block({40 title,41 children,42}: {43 title: string;44 children: React.ReactNode;45}) {46 return (47 <div className="border-t border-[var(--line)] py-5 first:border-t-0 first:pt-0">48 <h3 className="vp-display text-[15px] font-bold uppercase tracking-[-0.01em]">49 {title}50 </h3>51 <div className="mt-2 max-w-3xl text-[13.5px] leading-relaxed text-ink-2">52 {children}53 </div>54 </div>55 );56}5758export default function StudentPanel({ r }: { r: EstimateResult }) {59 const { lang, t } = useLang();60 const fr = lang === "fr";61 const parts = confidenceParts(r);62 const total = Math.max(63 5,64 Math.min(97, parts.base + parts.comps + parts.disp + parts.model + parts.width)65 );66 const modelPct = Math.round(r.modelWeight * 100);67 const compsPct = 100 - modelPct;6869 const confRows: [string, number][] = [70 [t("pedConfBase"), parts.base],71 [t("pedConfComps"), parts.comps],72 [t("pedConfDisp"), parts.disp],73 [t("pedConfModel"), parts.model],74 [t("pedConfWidth"), parts.width],75 ];7677 return (78 <section className="rise border border-[var(--line-strong)] bg-surface-2 p-5 sm:p-8">79 <header className="flex flex-wrap items-baseline justify-between gap-2 border-b-2 border-ink pb-3">80 <h2 className="vp-display text-[clamp(18px,2.6vw,24px)] font-bold uppercase tracking-[-0.02em]">81 {fr ? "Analyse pédagogique" : "Pedagogical analysis"}82 </h2>83 <span className="vp-mono text-[10px] uppercase tracking-[0.1em] text-accent-deep">84 {t("studentMode")} · IMM1003 / IMM103385 </span>86 </header>87 <p className="mt-4 max-w-3xl text-[13.5px] leading-relaxed text-ink-2">88 {t("pedIntro")}89 </p>9091 <div className="mt-6">92 {/* 1 — sélection des comparables */}93 <Block title={t("pedWhyCompsTitle")}>94 <p>{t("pedWhyComps")}</p>95 <p className="vp-mono mt-2 text-[11px] uppercase tracking-[0.05em] text-ink-3">96 {fr97 ? `Ici : ${r.nCompsUsed} comparables retenus${r.compsDispersionPct != null ? ` · dispersion médiane ±${r.compsDispersionPct} %` : ""}`98 : `Here: ${r.nCompsUsed} comparables kept${r.compsDispersionPct != null ? ` · median dispersion ±${r.compsDispersionPct}%` : ""}`}99 </p>100 </Block>101102 {/* 2 — pondération */}103 <Block title={t("pedWeightTitle")}>104 <p>{t("pedWeight")}</p>105 <Formula>106 poids = exp(−(distance∕1500 m)²) × exp(−(mois∕24)²) ×107 exp(−(Δaire∕25 %)²)108 </Formula>109 </Block>110111 {/* 3 — ajustements */}112 <Block title={t("pedAdjTitle")}>113 <ul className="list-none space-y-2 p-0">114 <li>115 <b className="text-ink">{t("adjTime")}</b> — {t("pedAdjTime")}116 </li>117 <li>118 <b className="text-ink">{t("adjArea")}</b> — {t("pedAdjArea")}119 </li>120 <li>121 <b className="text-ink">{t("adjAge")}</b> — {t("pedAdjAge")}122 </li>123 </ul>124 <Formula>125 prix ajusté = prix de vente + ajust. marché + ajust. superficie +126 ajust. âge127 </Formula>128 </Block>129130 {/* 4 — combinaison finale */}131 <Block title={t("pedBlendTitle")}>132 <p>{t("pedBlend")}</p>133 {/* barre de combinaison réelle */}134 <div className="mt-3 max-w-xl">135 <div className="flex h-8 overflow-hidden border border-[var(--line-strong)]">136 {r.modelEstimate != null && modelPct > 0 && (137 <span138 className="flex items-center justify-center bg-ink text-[10.5px] font-bold text-paper"139 style={{ width: `${modelPct}%` }}140 >141 {modelPct} %142 </span>143 )}144 {r.compsEstimate != null && compsPct > 0 && (145 <span146 className="flex items-center justify-center bg-accent text-[10.5px] font-bold text-white"147 style={{ width: `${compsPct}%` }}148 >149 {compsPct} %150 </span>151 )}152 </div>153 <p className="vp-mono mt-1.5 flex flex-wrap justify-between gap-2 text-[10px] uppercase tracking-[0.06em] text-ink-3">154 <span>155 {t("modelPart")} :{" "}156 {r.modelEstimate != null ? money(r.modelEstimate, lang) : "—"}157 </span>158 <span>159 {t("compsPart")} :{" "}160 {r.compsEstimate != null ? money(r.compsEstimate, lang) : "—"}161 </span>162 </p>163 </div>164 {r.modelEstimate != null && r.compsEstimate != null && (165 <Formula>166 {(r.modelWeight).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 2 })}167 × {money(r.modelEstimate, lang)} +{" "}168 {(1 - r.modelWeight).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 2 })}169 × {money(r.compsEstimate, lang)} ≈ {money(r.estimate, lang)}170 </Formula>171 )}172 </Block>173174 {/* 5 — indice de confiance décomposé */}175 <Block title={t("pedConfTitle")}>176 <div className="max-w-xl">177 {confRows.map(([label, v]) => (178 <div179 key={label}180 className="flex items-baseline justify-between gap-4 border-b border-[var(--line-soft)] py-1.5"181 >182 <span className="text-[13px]">{label}</span>183 <span184 className={`vp-mono text-[12.5px] font-bold ${v < 0 ? "text-[var(--danger)]" : "text-ink"}`}185 >186 {v >= 0 ? "+" : "−"}187 {Math.abs(v).toFixed(1).replace(/\.0$/, "")}188 </span>189 </div>190 ))}191 <div className="flex items-baseline justify-between gap-4 border-t-2 border-ink py-2">192 <span className="vp-display text-[14px] font-bold uppercase">193 {t("pedConfTotal")}194 </span>195 <span className="vp-mono text-[14px] font-bold text-ink">196 {Math.round(total)} / 100 → {r.confidenceLevel}197 </span>198 </div>199 <p className="vp-mono mt-1 text-[10px] uppercase tracking-[0.05em] text-ink-3">200 {fr201 ? "Niveaux : A ≥ 75 · B ≥ 60 · C ≥ 45 · D < 45 (borné à 5-97)"202 : "Levels: A ≥ 75 · B ≥ 60 · C ≥ 45 · D < 45 (clamped 5-97)"}203 </p>204 </div>205 </Block>206207 {/* 6 — concepts clés */}208 <Block title={t("pedConceptsTitle")}>209 <div className="grid gap-x-8 gap-y-4 sm:grid-cols-3">210 {(211 [212 ["pedConceptMarket", "pedConceptMarketDef"],213 ["pedConceptAsking", "pedConceptAskingDef"],214 ["pedConceptSale", "pedConceptSaleDef"],215 ] as const216 ).map(([k, d]) => (217 <div key={k}>218 <p className="vp-mono text-[11px] font-bold uppercase tracking-[0.08em] text-accent-deep">219 {t(k)}220 </p>221 <p className="mt-1 text-[13px] leading-snug">{t(d)}</p>222 </div>223 ))}224 </div>225 <p className="mt-4">226 <b className="text-ink">{t("pedRoleTitle")}</b> {t("pedRole")}227 </p>228 </Block>229 </div>230 </section>231 );232}233