SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
6.6 KB · 98 lines tsx
Raw Blame History
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