// ----------------------------------------------------------------------------- // Immo-Ka — Agrégateur de propriétés à vendre (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // pages/Stats.tsx : statistiques du marché agrégé (totaux + répartitions) // Les répartitions sont calculées à partir de /api/facets et /api/listings // (échantillon trié par prix) — le backend n'expose pas d'agrégats détaillés. // ----------------------------------------------------------------------------- import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { Facets, Listing, Stats, fetchFacets, fetchListings, fetchSources, fetchStats, registerSourceNames, sourceName, } from "../api"; function Bars({ rows, unit }: { rows: { key: string; n: number; href?: string }[]; unit?: string }) { const max = Math.max(1, ...rows.map((r) => r.n)); return (
{rows.map((r) => (
{r.href ? {r.key} : r.key}
{r.n.toLocaleString("fr-CA")}{unit ? {unit} : ""}
))}
); } export default function StatsPage() { const [stats, setStats] = useState(null); const [facets, setFacets] = useState(null); const [sample, setSample] = useState(null); useEffect(() => { fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {}); fetchStats().then(setStats).catch(() => {}); fetchFacets().then(setFacets).catch(() => {}); fetchListings({ sort: "price_asc" }, 2000, 0).then((r) => setSample(r.listings)).catch(() => {}); }, []); const byType = useMemo(() => { if (!sample) return []; const m = new Map(); for (const l of sample) if (l.property_type) m.set(l.property_type, (m.get(l.property_type) ?? 0) + 1); return [...m.entries()].map(([key, n]) => ({ key, n, href: `/?property_type=${encodeURIComponent(key)}` })) .sort((a, b) => b.n - a.n).slice(0, 12); }, [sample]); const byCity = useMemo(() => { if (!sample) return []; const m = new Map(); for (const l of sample) if (l.city) m.set(l.city, (m.get(l.city) ?? 0) + 1); return [...m.entries()].map(([key, n]) => ({ key, n, href: `/?city=${encodeURIComponent(key)}` })) .sort((a, b) => b.n - a.n).slice(0, 15); }, [sample]); const bySource = useMemo(() => { if (!facets) return []; return facets.sources.map((s) => ({ key: sourceName(s.source), n: s.n, href: `/?source=${encodeURIComponent(s.source)}` })) .sort((a, b) => b.n - a.n).slice(0, 15); }, [facets]); const tiles = [ { v: stats ? stats.total.toLocaleString("fr-CA") : "…", k: "Propriétés à vendre", hero: true }, { v: stats ? String(stats.sources) : "…", k: "Agences agrégées" }, { v: stats ? stats.cities.toLocaleString("fr-CA") : "…", k: "Villes couvertes" }, { v: stats?.avg_price != null ? `${Math.round(stats.avg_price).toLocaleString("fr-CA")} $` : "…", k: "Prix moyen demandé" }, { v: stats?.min_price != null ? `${Math.round(stats.min_price).toLocaleString("fr-CA")} $` : "…", k: "Prix minimum" }, { v: stats?.max_price != null ? `${Math.round(stats.max_price).toLocaleString("fr-CA")} $` : "…", k: "Prix maximum" }, ]; return (
Le marché agrégé

Statistiques

Portrait en direct des propriétés à vendre agrégées par Immo-Ka à travers toutes les agences connectées. Répartitions calculées sur un échantillon des annonces actives.

{tiles.map((t) => (
{t.v}
{t.k}
))}

Par type de propriété

Échantillon des annonces actives
{byType.length ? :

Chargement…

}

Par agence

Annonces actives (après déduplication)
{bySource.length ? :

Chargement…

}

Par ville

Top 15 · échantillon des annonces actives
{byCity.length ? :

Chargement…

}
{stats?.recent_syncs && stats.recent_syncs.length > 0 && (

Synchronisations récentes

Journal du moteur d'agrégation
{stats.recent_syncs.slice(0, 15).map((s, i) => ( ))}
AgenceTrouvéesAjoutéesMàJRetiréesQuand
{sourceName(s.source)} {s.found}{s.added}{s.updated}{s.removed} {new Date(s.ts * 1000).toLocaleString("fr-CA")}
)}

Mise à jour automatique — chaque fiche renvoie à l'annonce originale de l'agence.

); }