import type { Metadata } from "next"; import Link from "next/link"; import { cityMetrics, marketIndex, marketMetrics, regionMetrics } from "@/lib/queries"; import { dbAvailable, meta } from "@/lib/db"; import { fmtPrice, fmtInt, fmtPct, fmtDate } from "@/lib/format"; import { CATEGORY_LABEL } from "@/lib/normalize"; import { Empty, Notice, Stat } from "@/components/ui"; import { IndexChart } from "@/components/price-chart"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { title: "Marché et données", description: "Prix médians, nouvelles inscriptions, jours sur le marché et écart estimation/prix par région et municipalité du Québec." }; export default async function Marche({ searchParams }: { searchParams: Promise<{ marche?: string; region?: string; ville?: string; type?: string }> }) { const sp = await searchParams; if (!dbAvailable()) return
; const market = sp.marche === "location" ? "rent" : "sale"; const cat = sp.type ?? "all"; const scope = sp.ville ? "city" : sp.region ? "region" : "province"; const value = sp.ville ?? sp.region ?? "Québec"; const metrics = marketMetrics(scope, value, market); const all = metrics.find((m) => m.category === "all") ?? metrics[0]; const regions = regionMetrics(market, cat); const cities = sp.region ? cityMetrics(market, cat, 300).filter((c) => regions.length && true) : cityMetrics(market, cat, 30); const idx = market === "sale" ? marketIndex(cat === "condo" ? "condo" : cat === "plex" ? "plex" : "unifamilial") : []; const vp = (() => { try { return JSON.parse(meta("vp_marche_stats") ?? "{}"); } catch { return {}; } })() as { global?: Record; generated?: string; methods?: { method: string; mdape: number; medianRatio: number; n: number }[] }; const qs = (patch: Record) => { const u = new URLSearchParams(); const o = { marche: sp.marche, region: sp.region, ville: sp.ville, type: sp.type, ...patch }; for (const [k, v] of Object.entries(o)) if (v) u.set(k, v); return `/marche?${u}`; }; return (

Marché et données

{value}{scope !== "province" ? "" : " (ensemble des annonces Toveli)"} · {market === "rent" ? "location" : "achat"} · mis à jour le {fmtDate(meta("built_at"))}

AchatLocation
{(market === "sale" ? ["all", "maison", "condo", "plex", "terrain"] : ["all", "logement", "condo", "maison"]).map((c) => {c === "all" ? "Tous types" : CATEGORY_LABEL[c]})} {(sp.region || sp.ville) && × Tout le Québec}
{all ? (
{market === "sale" ? : }
) : Pas assez d'annonces pour calculer des statistiques fiables sur ce périmètre (minimum 5).} {metrics.length > 1 && (

Par catégorie

{market === "sale" && }{metrics.filter((m) => m.category !== "all").map((m) => {market === "sale" && })}
CatégorieAnnoncesMédianeP25–P75JoursEstim./prix
{CATEGORY_LABEL[m.category] ?? m.category}{fmtInt(m.n)}{fmtPrice(m.median_price)}{fmtPrice(m.p25_price, { compact: true })} – {fmtPrice(m.p75_price, { compact: true })}{m.median_days ?? "—"}{m.median_ratio ? fmtPct(m.median_ratio - 1) : "—"}
)} {idx.length > 1 &&

Indice de prix des ventes ({cat === "condo" ? "condos" : cat === "plex" ? "plex" : "unifamiliales"})

Base 100 = {idx[idx.length - 1].month}. Variation sur 12 mois : {idx.length > 12 ? fmtPct(idx[idx.length - 1].idx / idx[idx.length - 13].idx - 1) : "—"} · sur 5 ans : {idx.length > 60 ? fmtPct(idx[idx.length - 1].idx / idx[idx.length - 61].idx - 1) : "—"}. Source : base de transactions Toveli (prix au m² médian mensuel).

}

Régions

    {regions.map((r) =>
  • {r.scope_value}{fmtPrice(r.median_price, { compact: true })} · {fmtInt(r.n)}
  • )}

Municipalités{sp.region ? ` — ${sp.region}` : " les plus actives"}

    {cities.map((c) =>
  • {c.scope_value}{fmtPrice(c.median_price, { compact: true })} · {fmtInt(c.n)}
  • )}
{market === "sale" && vp.global && (

Qualité des estimations (rétro-test sur les annonces actives)

{vp.methods &&
{vp.methods.map((m) => )}
MéthodenMdAPERatio médian
{m.method}{fmtInt(m.n)}{fmtPct(m.mdape, 1, false)}{m.medianRatio?.toFixed(3)}
}

Le prix demandé n'est pas le prix de vente : ces écarts mesurent la cohérence estimation/annonce, pas l'erreur finale. Rétro-test généré le {fmtDate(vp.generated)}.

)}

Voir les annonces de ce périmètre →

); }