// Vrai-Prix — petits composants partagés de l'analyse IA du bâtiment (badges de couche, sections, tableau RCN, viewer JSON). "use client"; import { useState } from "react"; import type { Layer } from "@/lib/cost/types"; import type { CostEstimate } from "@/lib/cost/types"; import { money, num, pct } from "../fmt"; export function Sect({ num: n, kicker, title, id }: { num: string; kicker: string; title: string; id?: string }) { return (
{n} — {kicker}

{title}

); } /** §208 — les quatre couches, toujours nommées : OBSERVÉ · INFÉRÉ PAR IA · CALCULÉ · PRIX SOURCÉ (+ hypothèse). */ export function LayerBadge({ layer, fr }: { layer: Layer; fr: boolean }) { const map: Record = { observed: { fr: "Observé", en: "Observed", cls: "border-ink bg-ink text-paper" }, ai_inferred: { fr: "Inféré par IA", en: "AI-inferred", cls: "border-[var(--accent)] text-[var(--accent-deep)]" }, computed: { fr: "Calculé", en: "Computed", cls: "border-[var(--line-strong)] text-ink" }, sourced_price: { fr: "Prix sourcé", en: "Sourced price", cls: "border-[var(--green)] text-[var(--green-deep)]" }, assumption: { fr: "Hypothèse", en: "Assumption", cls: "border-[var(--amber)] bg-[var(--amber-soft)] text-[#8a5a12]" }, }; const m = map[layer]; return {fr ? m.fr : m.en}; } export function layerOf(source: string | null | undefined): Layer { switch (source) { case "MAMH": case "listing": case "cadastral": case "user": return "observed"; case "AI": case "photo": case "ai_estimated": return "ai_inferred"; case "derived": return "computed"; case "assumed": case "unknown": return "assumption"; default: return "computed"; } } export function ConfBar({ value, fr }: { value: number; fr: boolean }) { const p = Math.round(value <= 1 ? value * 100 : value); const tone = p >= 80 ? "bg-ink" : p >= 60 ? "bg-[var(--accent)]" : p >= 40 ? "bg-[var(--amber)]" : "bg-[var(--danger)]"; return ( {p} % ); } export function Stat({ k, v, sub, big }: { k: string; v: string; sub?: string; big?: boolean }) { return (

{k}

{v}

{sub ?

{sub}

: null}
); } /** Tableau RCN par catégorie → indirects → RCN → dépréciation → terrain → indication (§206). */ export function CostTable({ e, fr }: { e: CostEstimate; fr: boolean }) { const lang = fr ? "fr" : "en"; const row = (label: string, v: number | null, opts: { bold?: boolean; neg?: boolean; sep?: boolean; layer?: Layer } = {}) => ( {label}{opts.layer ? : null} {v == null ? "—" : `${opts.neg && v > 0 ? "−" : ""}${money(Math.abs(v), lang)}`} ); return (
{e.categories.map((c) => row(fr ? c.labelFr : c.labelEn, c.adjusted, { layer: "computed" }))} {row(fr ? "Coûts directs" : "Direct costs", e.directCost, { bold: true, sep: true })} {row(fr ? "Coûts indirects" : "Indirect costs", e.indirectCost, { layer: "assumption" })} {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" })} {row(fr ? `Profit (${num(e.input.params.profitPct, lang, 1)} %)` : `Profit (${num(e.input.params.profitPct, lang, 1)}%)`, e.contractorProfit, { layer: "assumption" })} {row(fr ? `Contingence (${num(e.input.params.contingencyPct, lang, 1)} %)` : `Contingency (${num(e.input.params.contingencyPct, lang, 1)}%)`, e.contingency, { layer: "assumption" })} {row(fr ? "COÛT DE REMPLACEMENT À NEUF (RCN)" : "REPLACEMENT COST NEW (RCN)", e.replacementCostNew, { bold: true, sep: true })} {row(fr ? "Détérioration physique" : "Physical deterioration", e.depreciation.physical, { neg: true, layer: "computed" })} {row(fr ? "Désuétude fonctionnelle" : "Functional obsolescence", e.depreciation.functional, { neg: true })} {row(fr ? "Désuétude externe" : "External obsolescence", e.depreciation.external, { neg: true })} {row(fr ? "Valeur dépréciée du bâtiment" : "Depreciated building value", e.depreciation.depreciatedImprovementValue, { bold: true, sep: true })} {row(fr ? "Valeur du terrain" : "Land value", e.landValue, { layer: e.input.land.source === "role" ? "observed" : e.input.land.source === "none" ? "assumption" : "observed" })} {row(fr ? "INDICATION PAR LE COÛT" : "COST APPROACH INDICATION", e.costApproachValue, { bold: true, sep: true })}
{fr ? "Coût de remplacement à neuf" : "Replacement cost new"}$

{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}`} {e.coverage.pricingCoverage < 1 ? ` · ${fr ? "couverture des coûts" : "cost coverage"} ${pct(e.coverage.pricingCoverage * 100, lang, 0, false)}` : ""}

); } export function JsonViewer({ value, fr }: { value: unknown; fr: boolean }) { const [open, setOpen] = useState(false); const txt = JSON.stringify(value, null, 2); return (
{open &&
{txt}
}
); }