Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 90.2%
JavaScript 3.5%
Python 3.4%
CSS 1.9%
HTML 0.6%
1// Vrai-Prix — petits composants partagés de l'analyse IA du bâtiment (badges de couche, sections, tableau RCN, viewer JSON).2"use client";3import { useState } from "react";4import type { Layer } from "@/lib/cost/types";5import type { CostEstimate } from "@/lib/cost/types";6import { money, num, pct } from "../fmt";78export function Sect({ num: n, kicker, title, id }: { num: string; kicker: string; title: string; id?: string }) {9 return (10 <header className="sec" id={id}>11 <span className="sec-num">{n} — {kicker}</span>12 <h2 className="vp-display mt-2 text-[clamp(20px,3vw,28px)] font-bold uppercase leading-[1.05] tracking-[-0.025em]">{title}</h2>13 </header>14 );15}1617/** §208 — les quatre couches, toujours nommées : OBSERVÉ · INFÉRÉ PAR IA · CALCULÉ · PRIX SOURCÉ (+ hypothèse). */18export function LayerBadge({ layer, fr }: { layer: Layer; fr: boolean }) {19 const map: Record<Layer, { fr: string; en: string; cls: string }> = {20 observed: { fr: "Observé", en: "Observed", cls: "border-ink bg-ink text-paper" },21 ai_inferred: { fr: "Inféré par IA", en: "AI-inferred", cls: "border-[var(--accent)] text-[var(--accent-deep)]" },22 computed: { fr: "Calculé", en: "Computed", cls: "border-[var(--line-strong)] text-ink" },23 sourced_price: { fr: "Prix sourcé", en: "Sourced price", cls: "border-[var(--green)] text-[var(--green-deep)]" },24 assumption: { fr: "Hypothèse", en: "Assumption", cls: "border-[var(--amber)] bg-[var(--amber-soft)] text-[#8a5a12]" },25 };26 const m = map[layer];27 return <span className={`vp-mono inline-block border px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em] ${m.cls}`}>{fr ? m.fr : m.en}</span>;28}2930export function layerOf(source: string | null | undefined): Layer {31 switch (source) {32 case "MAMH": case "listing": case "cadastral": case "user": return "observed";33 case "AI": case "photo": case "ai_estimated": return "ai_inferred";34 case "derived": return "computed";35 case "assumed": case "unknown": return "assumption";36 default: return "computed";37 }38}3940export function ConfBar({ value, fr }: { value: number; fr: boolean }) {41 const p = Math.round(value <= 1 ? value * 100 : value);42 const tone = p >= 80 ? "bg-ink" : p >= 60 ? "bg-[var(--accent)]" : p >= 40 ? "bg-[var(--amber)]" : "bg-[var(--danger)]";43 return (44 <span className="inline-flex items-center gap-1.5" title={fr ? `Confiance ${p} %` : `Confidence ${p}%`}>45 <span className="inline-block h-[5px] w-10 bg-[var(--line)]"><span className={`block h-full ${tone}`} style={{ width: `${p}%` }} /></span>46 <span className="vp-mono text-[10px] text-ink-3">{p} %</span>47 </span>48 );49}5051export function Stat({ k, v, sub, big }: { k: string; v: string; sub?: string; big?: boolean }) {52 return (53 <div className="kv-cell">54 <p className="k">{k}</p>55 <p className={`v ${big ? "text-[22px]" : ""}`}>{v}</p>56 {sub ? <p className="vp-mono mt-0.5 text-[10px] uppercase tracking-[0.05em] text-ink-3">{sub}</p> : null}57 </div>58 );59}6061/** Tableau RCN par catégorie → indirects → RCN → dépréciation → terrain → indication (§206). */62export function CostTable({ e, fr }: { e: CostEstimate; fr: boolean }) {63 const lang = fr ? "fr" : "en";64 const row = (label: string, v: number | null, opts: { bold?: boolean; neg?: boolean; sep?: boolean; layer?: Layer } = {}) => (65 <tr key={label} className={`${opts.bold ? "font-bold" : ""} ${opts.sep ? "border-t-2 border-ink" : ""}`}>66 <td>{label}{opts.layer ? <span className="ml-2"><LayerBadge layer={opts.layer} fr={fr} /></span> : null}</td>67 <td className={`vp-mono text-right ${opts.neg ? "text-[var(--danger)]" : ""}`}>{v == null ? "—" : `${opts.neg && v > 0 ? "−" : ""}${money(Math.abs(v), lang)}`}</td>68 </tr>69 );70 return (71 <div className="src-wrap">72 <table className="src-table">73 <thead><tr><th>{fr ? "Coût de remplacement à neuf" : "Replacement cost new"}</th><th className="text-right">$</th></tr></thead>74 <tbody>75 {e.categories.map((c) => row(fr ? c.labelFr : c.labelEn, c.adjusted, { layer: "computed" }))}76 {row(fr ? "Coûts directs" : "Direct costs", e.directCost, { bold: true, sep: true })}77 {row(fr ? "Coûts indirects" : "Indirect costs", e.indirectCost, { layer: "assumption" })}78 {row(fr ? `Frais généraux (${num(e.input.params.overheadPct, lang, 1)} %)` : `Overhead (${num(e.input.params.overheadPct, lang, 1)}%)`, e.contractorOverhead, { layer: "assumption" })}79 {row(fr ? `Profit (${num(e.input.params.profitPct, lang, 1)} %)` : `Profit (${num(e.input.params.profitPct, lang, 1)}%)`, e.contractorProfit, { layer: "assumption" })}80 {row(fr ? `Contingence (${num(e.input.params.contingencyPct, lang, 1)} %)` : `Contingency (${num(e.input.params.contingencyPct, lang, 1)}%)`, e.contingency, { layer: "assumption" })}81 {row(fr ? "COÛT DE REMPLACEMENT À NEUF (RCN)" : "REPLACEMENT COST NEW (RCN)", e.replacementCostNew, { bold: true, sep: true })}82 {row(fr ? "Détérioration physique" : "Physical deterioration", e.depreciation.physical, { neg: true, layer: "computed" })}83 {row(fr ? "Désuétude fonctionnelle" : "Functional obsolescence", e.depreciation.functional, { neg: true })}84 {row(fr ? "Désuétude externe" : "External obsolescence", e.depreciation.external, { neg: true })}85 {row(fr ? "Valeur dépréciée du bâtiment" : "Depreciated building value", e.depreciation.depreciatedImprovementValue, { bold: true, sep: true })}86 {row(fr ? "Valeur du terrain" : "Land value", e.landValue, { layer: e.input.land.source === "role" ? "observed" : e.input.land.source === "none" ? "assumption" : "observed" })}87 {row(fr ? "INDICATION PAR LE COÛT" : "COST APPROACH INDICATION", e.costApproachValue, { bold: true, sep: true })}88 </tbody>89 </table>90 <p className="vp-mono mt-2 text-[10px] uppercase tracking-[0.05em] text-ink-3">91 {fr ? `Fourchette RCN : ${money(e.range.p10, lang)} – ${money(e.range.p90, lang)} (P10-P90) · ${num(e.perSqft, lang, 0)} $/pi² · prix du ${e.priceDate}` : `RCN range: ${money(e.range.p10, lang)} – ${money(e.range.p90, lang)} (P10-P90) · $${num(e.perSqft, lang, 0)}/sq ft · prices as of ${e.priceDate}`}92 {e.coverage.pricingCoverage < 1 ? ` · ${fr ? "couverture des coûts" : "cost coverage"} ${pct(e.coverage.pricingCoverage * 100, lang, 0, false)}` : ""}93 </p>94 </div>95 );96}9798export function JsonViewer({ value, fr }: { value: unknown; fr: boolean }) {99 const [open, setOpen] = useState(false);100 const txt = JSON.stringify(value, null, 2);101 return (102 <div>103 <button type="button" className="btn btn-ghost" onClick={() => setOpen((o) => !o)}>{open ? (fr ? "Masquer le JSON" : "Hide JSON") : (fr ? "Voir les données techniques (JSON)" : "View technical data (JSON)")}</button>104 {open && <pre className="vp-mono mt-3 max-h-[520px] overflow-auto border border-[var(--line)] bg-surface-2 p-3 text-[10.5px] leading-snug">{txt}</pre>}105 </div>106 );107}108