// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/KaScoresCard.tsx : « KA Scores » — 5 cercles compacts (Marche, // Transport, Vélo, Calme, Services), score personnalisé selon les // pondérations de l'utilisateur, détail honnête en accordéon. // ----------------------------------------------------------------------------- import { Link } from "react-router-dom"; import { KaScores, KA_DEFAULT_WEIGHTS, fmtDist, kaGlobal, kaLabel, kaWeights } from "../api"; import { kaTint } from "../components/KaScoreBadge"; import { IcoLayers } from "../components/Icons"; import { Accordion, SectionCard, SourceLine } from "./ui"; const CAT: Record = { epicerie: "Épicerie", pharmacie: "Pharmacie", parc: "Parc", cafe: "Café", ecole: "École", clinique: "Clinique", garderie: "Garderie", depanneur: "Dépanneur", gym: "Gym", bibliotheque: "Bibliothèque", }; const FAM: Record = { commerces: "commerces", sante: "santé", education: "éducation", loisirs: "loisirs" }; function Cercle({ score, nom, note }: { score: number | null; nom: string; note?: string }) { const r = 22, c = 2 * Math.PI * r; const part = score == null ? 0 : Math.max(0, Math.min(1, score / 100)); return (
{score == null ? "—" : Math.round(score)}
{nom}
{score == null ? (note ?? "n/d") : kaLabel(score)}
); } export default function KaScoresCard({ ks }: { ks: KaScores }) { const weights = kaWeights(); const custom = JSON.stringify(weights) !== JSON.stringify(KA_DEFAULT_WEIGHTS); const perso = custom ? kaGlobal(ks, weights) : null; const d = ks.details ?? {}; const walkCats = (d.walk?.cats ?? []).filter((c) => c.dist_m != null).slice(0, 6); return ( } sub={ks.global != null ? `Global ${Math.round(ks.global)} · ${kaLabel(ks.global)}${perso != null ? ` · selon vos priorités : ${Math.round(perso)}` : ""}` : undefined}>
{walkCats.length > 0 && (

Marche

    {walkCats.map((c) =>
  • {CAT[c.cat] ?? c.cat} : {fmtDist(c.dist_m as number)}
  • )}
)}

Transport

    {d.transit?.arret_bus_m != null &&
  • Arrêt de bus à {fmtDist(d.transit.arret_bus_m)}
  • } {d.transit?.station_metro_m != null &&
  • Station de métro à {fmtDist(d.transit.station_metro_m)}
  • } {d.transit?.pmd_percentile != null &&
  • Desserte : {d.transit.pmd_percentile}ᵉ percentile canadien (StatCan)
  • } {d.transit?.raison &&
  • {d.transit.raison}
  • }

Vélo

    {d.bike?.km_cyclables_1km != null &&
  • {d.bike.km_cyclables_1km.toLocaleString("fr-CA")} km de voies cyclables à moins de 1 km
  • } {d.bike?.raison &&
  • {d.bike.raison}
  • } {d.bike?.note &&
  • {d.bike.note}
  • }

Calme

    {(d.calme?.sources_bruit ?? []).length === 0 &&
  • Aucune source de bruit majeure détectée à proximité
  • } {(d.calme?.sources_bruit ?? []).map((s) =>
  • {s.source}{s.dist_m != null ? ` à ${fmtDist(s.dist_m)}` : ""}
  • )} {(d.calme?.bonus_parc ?? 0) > 0 &&
  • Parc à proximité
  • } {d.calme?.note &&
  • {d.calme.note}
  • }
{d.services?.familles && ( <>

Services

    {Object.entries(d.services.familles).map(([f, n]) =>
  • {n} {FAM[f] ?? f} dans le secteur
  • )}
)}

Scores 0-100 calculés depuis OpenStreetMap et les mesures de proximité de Statistique Canada — le Calme est une estimation d'environnement, pas une mesure sonore. Barème {ks.version}, calculé le{" "} {new Date(ks.computed_at * 1000).toLocaleDateString("fr-CA")}.{" "} Méthodologie et réglage de vos priorités

); }