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 — méthode du coût : marqueurs de provenance (● observé ◐ calculé ○ indexé ◇ référence/hypothèse · AI),2// badges de couche (OBSERVÉ · INFÉRÉ PAR IA · CALCULÉ · PRIX SOURCÉ · HYPOTHÈSE) et origine des caractéristiques.3"use client";4import type { AttributeSource, Layer, PriceKind } from "@/lib/cost/types";56export function kindGlyph(kind: PriceKind | null | undefined): string {7 switch (kind) {8 case "observed": case "official": return "●";9 case "derived": return "◐";10 case "indexed": return "○";11 case "reference": case "assumption": return "◇";12 default: return "·";13 }14}1516export function kindLabel(kind: PriceKind | null | undefined, fr: boolean): string {17 switch (kind) {18 case "observed": return fr ? "Observé directement" : "Directly observed";19 case "official": return fr ? "Publication officielle" : "Official publication";20 case "derived": return fr ? "Calculé depuis composants" : "Computed from components";21 case "indexed": return fr ? "Indexé" : "Indexed";22 case "reference": return fr ? "Référence interne (hypothèse)" : "Internal reference (assumption)";23 case "assumption": return fr ? "Hypothèse" : "Assumption";24 default: return fr ? "Donnée non disponible" : "Data not available";25 }26}2728export function kindTooltip(p: { kind: PriceKind | null | undefined; source?: string | null; observedAt?: string | null; note?: string | null }, fr: boolean): string {29 const d = p.observedAt ? fmtDate(p.observedAt, fr) : null;30 switch (p.kind) {31 case "observed": return fr ? `Observé directement chez ${p.source ?? "un fournisseur"}${d ? ` le ${d}` : ""}.` : `Directly observed at ${p.source ?? "a supplier"}${d ? ` on ${d}` : ""}.`;32 case "official": return fr ? `Taux publié par ${p.source ?? "une source officielle"}${d ? ` (en vigueur le ${d})` : ""}.` : `Rate published by ${p.source ?? "an official source"}${d ? ` (effective ${d})` : ""}.`;33 case "indexed": return p.note ?? (fr ? "Observation ancienne actualisée avec un indice Statistique Canada." : "Older observation updated with a Statistics Canada index.");34 case "derived": return fr ? "Calculé à partir de composants eux-mêmes sourcés." : "Computed from components that are themselves sourced.";35 case "reference": return fr ? "Prix de référence interne — hypothèse documentée, non observé." : "Internal reference price — documented assumption, not observed.";36 case "assumption": return p.note ?? (fr ? "Hypothèse documentée (productivité, pourcentage)." : "Documented assumption (productivity, percentage).");37 default: return fr ? "Donnée non disponible." : "Data not available.";38 }39}4041export function fmtDate(iso: string | null | undefined, fr: boolean): string {42 if (!iso) return "—";43 const d = new Date(iso.length <= 10 ? `${iso}T12:00:00` : iso);44 if (Number.isNaN(d.getTime())) return iso;45 return d.toLocaleDateString(fr ? "fr-CA" : "en-CA", { year: "numeric", month: "long", day: "numeric" });46}4748/** Icône de provenance avec infobulle native (title). */49export function KindIcon({ kind, fr, source, observedAt, note, className = "" }: { kind: PriceKind | null | undefined; fr: boolean; source?: string | null; observedAt?: string | null; note?: string | null; className?: string }) {50 const tone = kind === "observed" || kind === "official" ? "text-ink" : kind === "indexed" || kind === "derived" ? "text-accent" : "text-[var(--amber)]";51 return (52 <span className={`vp-mono inline-block text-[12px] leading-none ${tone} ${className}`} title={kindTooltip({ kind, source, observedAt, note }, fr)} aria-label={kindLabel(kind, fr)}>53 {kindGlyph(kind)}54 </span>55 );56}5758/** Légende des icônes de provenance. */59export function KindLegend({ fr }: { fr: boolean }) {60 const rows: [PriceKind, string][] = [["observed", fr ? "Observé directement" : "Observed directly"], ["derived", fr ? "Calculé depuis composants" : "Computed from components"], ["indexed", fr ? "Indexé (StatCan)" : "Indexed (StatCan)"], ["reference", fr ? "Référence interne · hypothèse" : "Internal reference · assumption"]];61 return (62 <div className="vp-mono flex flex-wrap gap-x-4 gap-y-1 text-[10px] uppercase tracking-[0.06em] text-ink-3">63 {rows.map(([k, l]) => (64 <span key={k} className="inline-flex items-center gap-1.5"><KindIcon kind={k} fr={fr} />{l}</span>65 ))}66 <span className="inline-flex items-center gap-1.5"><span className="vp-mono text-[9px] font-bold text-accent">AI</span>{fr ? "Classifié / inféré par IA" : "AI-classified / inferred"}</span>67 </div>68 );69}7071/** Badge de couche (§208) : OBSERVÉ · INFÉRÉ PAR IA · CALCULÉ · PRIX SOURCÉ · HYPOTHÈSE. */72export function LayerBadge({ layer, fr }: { layer: Layer; fr: boolean }) {73 const map: Record<Layer, { fr: string; en: string; cls: string }> = {74 observed: { fr: "Observé", en: "Observed", cls: "border-ink bg-ink text-paper" },75 ai_inferred: { fr: "Inféré par IA", en: "AI-inferred", cls: "border-accent text-accent" },76 computed: { fr: "Calculé", en: "Computed", cls: "border-[var(--line-strong)] text-ink" },77 sourced_price: { fr: "Prix sourcé", en: "Sourced price", cls: "border-[var(--line-strong)] text-ink-2" },78 assumption: { fr: "Hypothèse", en: "Assumption", cls: "border-[var(--amber)] text-[#8a5a12] bg-[var(--amber-soft)]" },79 };80 const m = map[layer];81 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>;82}8384/** Origine d'une caractéristique du bâtiment : MAMH / saisi / IA / dérivé / supposé / annonce. */85export function AttrBadge({ source, fr }: { source: AttributeSource | null | undefined; fr: boolean }) {86 const map: Record<string, { fr: string; en: string; cls: string }> = {87 MAMH: { fr: "MAMH", en: "MAMH", cls: "border-ink bg-ink text-paper" },88 cadastral: { fr: "Cadastre", en: "Cadastral", cls: "border-ink bg-ink text-paper" },89 listing: { fr: "Annonce", en: "Listing", cls: "border-[var(--line-strong)] text-ink" },90 user: { fr: "Saisi", en: "Entered", cls: "border-[var(--line-strong)] text-ink" },91 AI: { fr: "IA", en: "AI", cls: "border-accent text-accent" },92 derived: { fr: "Dérivé", en: "Derived", cls: "border-[var(--line)] text-ink-2" },93 assumed: { fr: "Supposé", en: "Assumed", cls: "border-[var(--amber)] text-[#8a5a12] bg-[var(--amber-soft)]" },94 };95 const m = map[source ?? "assumed"] ?? map.assumed;96 return <span className={`vp-mono inline-block border px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em] ${m.cls}`} title={fr ? "Origine de la caractéristique" : "Attribute origin"}>{fr ? m.fr : m.en}</span>;97}98