// -----------------------------------------------------------------------------
// 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.
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
Agence Trouvées Ajoutées MàJ Retirées Quand
{stats.recent_syncs.slice(0, 15).map((s, i) => (
{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.
);
}