// ----------------------------------------------------------------------------- // Lou-Ka — Location court terme (chalets, séjours, hébergements) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // ctapi.ts : types + client API /api/ct/* — sous-système séparé du long terme. // ----------------------------------------------------------------------------- export interface CtDetails { spa?: boolean; pool?: boolean; waterfront?: boolean; sauna?: boolean; wifi?: boolean; fireplace?: boolean; ev_charger?: boolean; [k: string]: unknown; } export interface CtListing { uid: string; source: string; external_id: string; url: string; title: string; property_type: string; address: string; city: string; region: string; price_night: number | null; price_label: string; capacity: number | null; bedrooms: number | null; beds: number | null; bathrooms: number | null; pets: string | null; // "oui" | "non" | "conditions" citq: string | null; // numéro d'enregistrement CITQ rating: number | null; // sur 5 reviews: number | null; description: string; amenities: string[]; details: CtDetails; images: string[]; lat: number | null; lng: number | null; first_seen?: number; last_seen?: number; active?: number; } export interface CtFacets { regions: { region: string; n: number }[]; types: { type: string; n: number }[]; sources: { source: string; n: number }[]; } export interface CtStats { total: number; sources: number; regions: number; avg_night: number | null; geocoded: number; } export interface CtSource { id: string; name: string; url: string; category: string; status?: string; connector: boolean; active_listings: number; last_sync: number | null; } export interface CtFilters { region?: string; city?: string; type?: string; source?: string; price_min?: string; price_max?: string; capacity_min?: string; bedrooms_min?: string; pets?: string; // "oui" spa?: string; // "1" waterfront?: string; // "1" q?: string; sort?: string; // recent | prix | prix_desc | note } export interface CtPriceContext { segment: string; n: number; median: number; p25: number; p75: number; deviation: number | null; // (prix − médiane) / médiane verdict: "sous" | "dans" | "dessus" | null; percentile: number; // position du prix dans le segment (0-100) histogram: { x0: number; x1: number; n: number }[]; } export interface CtContext { price: CtPriceContext | null; similar: (CtListing & { distance_km?: number })[]; } const CT_SOURCE_NAMES: Record = {}; const CT_SOURCE_CATS: Record = {}; export function registerCtSourceNames(sources: CtSource[]) { for (const s of sources) { CT_SOURCE_NAMES[s.id] = s.name; CT_SOURCE_CATS[s.id] = s.category; } } export function ctSourceName(id: string): string { return CT_SOURCE_NAMES[id] ?? id; } export function ctSourceCategory(id: string): string { return CT_SOURCE_CATS[id] ?? ""; } async function get(path: string): Promise { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 20000); try { const res = await fetch(path, { signal: ctrl.signal }); if (!res.ok) throw new Error(`API ${res.status} — ${path}`); return (await res.json()) as T; } finally { clearTimeout(timer); } } export function fetchCtListings(f: CtFilters, limit?: number, offset?: number) { const params = new URLSearchParams(); for (const [k, v] of Object.entries(f)) if (v) params.set(k, v); if (limit != null) params.set("limit", String(limit)); if (offset) params.set("offset", String(offset)); return get<{ total: number; count: number; listings: CtListing[] }>( `/api/ct/listings?${params}`); } export const fetchCtListing = (uid: string) => get(`/api/ct/listings/${encodeURIComponent(uid)}`); export const fetchCtContext = (uid: string) => get(`/api/ct/listings/${encodeURIComponent(uid)}/context`); export const fetchCtFacets = () => get("/api/ct/facets"); export const fetchCtStats = () => get("/api/ct/stats"); export const fetchCtSources = () => get<{ sources: CtSource[] }>("/api/ct/sources"); export const fmtNight = (p: number | null, label?: string) => p != null ? `${p.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $` : label || "Prix sur demande";