// Vrai-Prix — méthode du coût : page d'atterrissage /cout (héro, KPI « Coût aujourd'hui », indices, // explorateur de coûts unitaires, observatoire des matériaux, assemblages populaires, régions, méthode, sources, limites). "use client"; import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useLang } from "@/components/LangContext"; import SearchBox from "@/components/SearchBox"; import type { CostOverview, PriceKind } from "@/lib/cost/types"; import { UNIT_LABELS } from "@/lib/cost/units"; import { categoryLabel } from "@/lib/cost/taxonomy"; import { money, num, pct } from "@/components/avendre/fmt"; import { IndexLines, Sparkline } from "./Charts"; import { fmtDate, KindIcon, KindLegend } from "./Provenance"; import { Drawer, PriceSourceCard } from "./SourceDrawer"; interface ItemRow { code: string; nameFr: string; nameEn: string; unit: string; category: string; price: number | null; low: number | null; high: number | null; kind: PriceKind | null; sourceCount: number; observedAt: string | null; confidence: number | null; note: string | null; sources: { source: string; price: number; url: string | null; observedAt: string; sourceUnit: string; conversionFactor: number; regular: boolean }[] } export function Sect({ num, kicker, title, lede, id }: { num: string; kicker: string; title: string; lede?: string; id?: string }) { return (
{num} {kicker}

{title}

{lede &&

{lede}

}
); } function Kpi({ k, v, sub, na }: { k: string; v: string | null; sub?: string | null; na: string }) { return (

{k}

{v ?? na}

{sub &&

{sub}

}
); } export default function Landing({ overview }: { overview: CostOverview | null }) { const { lang } = useLang(); const fr = lang === "fr"; const router = useRouter(); const na = fr ? "Donnée non disponible" : "Data not available"; const k = overview?.kpis; const [q, setQ] = useState(""); const [items, setItems] = useState(null); const [searching, setSearching] = useState(null); const [drawer, setDrawer] = useState(null); const timer = useRef | null>(null); useEffect(() => { if (timer.current) clearTimeout(timer.current); const query = q.trim(); if (query.length < 2) return; timer.current = setTimeout(async () => { try { const r = await fetch(`/api/cost/items?q=${encodeURIComponent(query)}`); const d = (await r.json()) as { items: ItemRow[] }; setItems(d.items ?? []); setSearching(query); } catch { setItems([]); setSearching(query); } }, 250); return () => { if (timer.current) clearTimeout(timer.current); }; }, [q]); const loading = q.trim().length >= 2 && searching !== q.trim(); const indexSeries = (overview?.indices ?? []) .filter((s) => ["statcan:18100289:10:1:1", "statcan:18100289:9:1:1", "statcan:18100289:10:5:1", "statcan:18100289:10:1:8", "statcan:18100289:10:1:19"].includes(s.code)) .map((s) => ({ label: `${s.geography.split(",")[0]} — ${s.division === "aggregate" || s.division.startsWith("Agr") ? s.buildingType : s.division}`.slice(0, 40), points: s.points.map((p) => ({ period: p.period, value: p.value })) })); const unitL = (u: string) => UNIT_LABELS[u as keyof typeof UNIT_LABELS]?.[fr ? "fr" : "en"] ?? u; return (
{/* ================= héro ================= */}
{fr ? "Troisième lecture de la valeur" : "Third reading of value"}
{fr ? "Méthode du coût" : "Cost approach"}

{fr ? <>Combien coûterait ce bâtiment
à reconstruire aujourd'hui ? : <>What would this building cost
to rebuild today?}

{fr ? "Décomposez le coût de remplacement à neuf, composante par composante, puis mesurez la dépréciation et ajoutez la valeur du terrain. Chaque hypothèse et chaque source sont visibles." : "Break the replacement cost new down component by component, then measure depreciation and add the land value. Every assumption and every source is visible."}

{fr ? "Mode propriété — une adresse du rôle d'évaluation" : "Property mode — an address from the assessment roll"}

router.push(`/cout?property=${encodeURIComponent(s.id)}`)} placeholder={fr ? "Entrez une adresse (ex. 35 rue Servantes, Gatineau)" : "Enter an address (e.g. 35 rue Servantes, Gatineau)"} />
{fr ? "Mode construction (sans adresse)" : "Construction mode (no address)"} {fr ? "Depuis une annonce à vendre" : "From a listing for sale"} →

{fr ? "Estimation indicative — les coûts présentés sont des estimations statistiques et ne constituent ni une soumission d'entrepreneur ni une évaluation professionnelle certifiée (OEAQ)." : "Indicative estimate — the costs shown are statistical estimates and constitute neither a contractor's bid nor a certified professional appraisal (OEAQ)."}

{/* ================= KPI ================= */}

{fr ? "Indices des prix de la construction de bâtiments — 2021 → présent (StatCan 18-10-0289-01, base 2017 = 100)" : "Building construction price indexes — 2021 → present (StatCan 18-10-0289-01, 2017 = 100)"}

{indexSeries.length ? :

{na}

}
{/* ================= explorateur ================= */}
setQ(e.target.value)} placeholder={fr ? "Rechercher : gypse, 2 x 6, bardeau, R24, fenêtre…" : "Search: drywall, 2 x 6, shingle, R24, window…"} className="vp-input text-base" aria-label={fr ? "Rechercher un coût unitaire" : "Search a unit cost"} />
{q.trim().length >= 2 && (
{loading && } {!loading && items?.length === 0 && } {!loading && items?.map((it) => ( setDrawer(it)}> ))}
{fr ? "Article" : "Item"}{fr ? "Prix" : "Price"}{fr ? "Unité" : "Unit"}{fr ? "Prov." : "Prov."}{fr ? "Sources" : "Sources"}{fr ? "Mise à jour" : "Updated"}
{fr ? "Recherche…" : "Searching…"}
{fr ? "Aucun article." : "No item."}
{fr ? it.nameFr : it.nameEn}
{it.code} · {categoryLabel(it.category as never, fr)}
{it.price != null ? `${num(it.price, lang, 2)} $` : na} / {unitL(it.unit)} {it.sourceCount} {it.observedAt ?? "—"}
)} setDrawer(null)} title={fr ? "Source du prix" : "Price source"} fr={fr}> {drawer && ( )} {drawer && {fr ? "Voir les assemblages qui l'utilisent" : "See assemblies using it"} →}
{/* ================= observatoire matériaux ================= */}
{(overview?.materials ?? []).map((m) => (

{fr ? m.nameFr : m.nameEn}

{m.price != null ? `${num(m.price, lang, 2)} $` : {na}} / {unitL(m.unit)}

{m.sourceCount} {fr ? "source(s)" : "source(s)"} · {m.observedAt ?? (fr ? "référence" : "reference")}

))} {!overview?.materials?.length &&

{na}

}
{/* ================= assemblages populaires ================= */}
{(overview?.popularAssemblies ?? []).map((a) => (

{a.code}

{fr ? a.nameFr : a.nameEn}

{num(a.direct, lang, 2)} $ / {unitL(a.unit)}

{fr ? "Matériaux" : "Materials"}
{num(a.material, lang, 2)}
{fr ? "Main-d'œuvre" : "Labour"}
{num(a.labour, lang, 2)}
{fr ? "Équip." : "Equip."}
{num(a.equipment, lang, 2)}

{fr ? "Part observée" : "Observed share"} : {Math.round(a.observedShare * 100)} %

))}
{fr ? "Tout le catalogue d'assemblages" : "Full assembly catalogue"} →
{/* ================= régions ================= */}
{(overview?.locations ?? []).map((l) => ( ))}
{fr ? "Région" : "Region"}{fr ? "Matériaux" : "Materials"}{fr ? "Main-d'œuvre" : "Labour"}{fr ? "Équipement" : "Equipment"}{fr ? "Global" : "Overall"}{fr ? "Confiance" : "Confidence"}
{fr ? l.nameFr : l.nameEn} {l.code} × {num(l.materialFactor, lang, 2)} × {num(l.labourFactor, lang, 2)} × {num(l.equipmentFactor, lang, 2)} × {num(l.overallFactor, lang, 3)} {l.confidence} / 100
{/* ================= méthode en 4 étapes ================= */}
    {(fr ? [["Décrire le bâtiment", "Type, aire, étages, sous-sol, garage, revêtements, toiture, fenêtres, mécanique, cuisine, salles de bain — chaque caractéristique porte son origine (rôle, saisie, IA, hypothèse)."], ["Quantifier et chiffrer à neuf", "Le moteur de géométrie dérive les quantités d'assemblages (formules visibles) ; chaque assemblage = matériaux + heures × taux + équipement, localisés, puis indirects, frais généraux, profit, contingence → RCN."], ["Déprécier", "Physique (âge-vie ou composante par composante selon la condition), fonctionnelle (curable / incurable), externe. Rien n'est calculé sans preuve : l'externe vaut 0 par défaut."], ["Ajouter le terrain, comparer", "Valeur du terrain au rôle (ou saisie) + bâtiment déprécié = indication par le coût. Confrontée aux lectures hédonique et comparables, aux benchmarks, jamais moyennée."]] : [["Describe the building", "Type, area, storeys, basement, garage, cladding, roof, windows, mechanical, kitchen, bathrooms — each attribute carries its origin (roll, entered, AI, assumption)."], ["Quantify and price new", "The geometry engine derives assembly quantities (visible formulas); each assembly = materials + hours × rate + equipment, localised, then indirects, overhead, profit, contingency → RCN."], ["Depreciate", "Physical (age-life or component by component by condition), functional (curable / incurable), external. Nothing is computed without evidence: external defaults to 0."], ["Add land, compare", "Land value from the roll (or entered) + depreciated building = cost indication. Confronted with the hedonic and comparables readings and benchmarks, never averaged."]] ).map(([t, b], i) => (
  1. 0{i + 1}

    {t}

    {b}

  2. ))}
{/* ================= sources + limites ================= */}
    {(overview?.sources ?? []).filter((s) => s.active || s.observations > 0).map((s) => (
  • {s.url ? {s.name} ↗ : s.name} {s.type} · {s.license.replace("_", " ")} · {s.observations} obs. · {s.lastSync ? fmtDate(s.lastSync, fr) : (fr ? "jamais synchronisé" : "never synced")}
  • ))}

{fr ? "CCQ · APCHQ · Statistique Canada · Détaillants publics (Canac, BMR, Patrick Morin) · Altus Group (import licencié) · Sources autorisées. Une source « unknown » n'est jamais redistribuée avant validation ; Firecrawl n'est qu'un outil d'extraction et ne confère aucun droit de réutilisation." : "CCQ · APCHQ · Statistics Canada · Public retailers (Canac, BMR, Patrick Morin) · Altus Group (licensed import) · Authorised sources. An 'unknown' source is never redistributed before validation; Firecrawl is only an extraction tool and grants no reuse right."}

    {(fr ? ["Coût de remplacement (composantes modernes équivalentes), pas de reproduction à l'identique : un bâtiment ancien est chiffré avec un 2 × 6 R24, pas avec ses matériaux d'origine.", "Les quantités d'assemblages découlent d'hypothèses de prise de quantités (rapport de forme 1,5, hauteur d'étage 9 pi, 16 po c/c) : vous pouvez les remplacer une à une.", "Les prix de référence internes (béton livré, fermes, fenêtres sur mesure…) sont des hypothèses documentées ; ils abaissent la couverture observée et l'indice de confiance.", "La valeur du terrain au rôle n'est pas la valeur marchande du terrain : elle date de 18 à 24 mois avant l'entrée en vigueur du rôle.", "La dépréciation par composante dépend de la condition saisie ou observée ; sans information, l'âge chronologique plafonné est utilisé.", "Aucune moyenne des trois approches : la comparaison est l'objet de l'analyse, pas un chiffre unique."] : ["Replacement cost (modern equivalent components), not identical reproduction: an old building is priced with a 2 × 6 R24 wall, not with its original materials.", "Assembly quantities follow take-off assumptions (aspect ratio 1.5, 9 ft storey height, 16 in o.c.): you can replace them one by one.", "Internal reference prices (delivered concrete, trusses, custom windows…) are documented assumptions; they lower observed coverage and the confidence index.", "The land value on the roll is not the land's market value: it dates from 18 to 24 months before the roll took effect.", "Component depreciation depends on entered or observed condition; without information, capped chronological age is used.", "No averaging of the three approaches: the comparison is the object of the analysis, not a single number."] ).map((t) => (
  • {t}
  • ))}
); } export { money };