Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1// -----------------------------------------------------------------------------2// Lou-Ka — Location court terme (chalets, séjours, hébergements)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// ctapi.ts : types + client API /api/ct/* — sous-système séparé du long terme.5// -----------------------------------------------------------------------------67export interface CtDetails {8 spa?: boolean;9 pool?: boolean;10 waterfront?: boolean;11 sauna?: boolean;12 wifi?: boolean;13 fireplace?: boolean;14 ev_charger?: boolean;15 [k: string]: unknown;16}1718export interface CtListing {19 uid: string;20 source: string;21 external_id: string;22 url: string;23 title: string;24 property_type: string;25 address: string;26 city: string;27 region: string;28 price_night: number | null;29 price_label: string;30 capacity: number | null;31 bedrooms: number | null;32 beds: number | null;33 bathrooms: number | null;34 pets: string | null; // "oui" | "non" | "conditions"35 citq: string | null; // numéro d'enregistrement CITQ36 rating: number | null; // sur 537 reviews: number | null;38 description: string;39 amenities: string[];40 details: CtDetails;41 images: string[];42 lat: number | null;43 lng: number | null;44 first_seen?: number;45 last_seen?: number;46 active?: number;47}4849export interface CtFacets {50 regions: { region: string; n: number }[];51 types: { type: string; n: number }[];52 sources: { source: string; n: number }[];53}5455export interface CtStats {56 total: number;57 sources: number;58 regions: number;59 avg_night: number | null;60 geocoded: number;61}6263export interface CtSource {64 id: string;65 name: string;66 url: string;67 category: string;68 status?: string;69 connector: boolean;70 active_listings: number;71 last_sync: number | null;72}7374export interface CtFilters {75 region?: string;76 city?: string;77 type?: string;78 source?: string;79 price_min?: string;80 price_max?: string;81 capacity_min?: string;82 bedrooms_min?: string;83 pets?: string; // "oui"84 spa?: string; // "1"85 waterfront?: string; // "1"86 q?: string;87 sort?: string; // recent | prix | prix_desc | note88}8990export interface CtPriceContext {91 segment: string;92 n: number;93 median: number;94 p25: number;95 p75: number;96 deviation: number | null; // (prix − médiane) / médiane97 verdict: "sous" | "dans" | "dessus" | null;98 percentile: number; // position du prix dans le segment (0-100)99 histogram: { x0: number; x1: number; n: number }[];100}101102export interface CtContext {103 price: CtPriceContext | null;104 similar: (CtListing & { distance_km?: number })[];105}106107const CT_SOURCE_NAMES: Record<string, string> = {};108const CT_SOURCE_CATS: Record<string, string> = {};109export function registerCtSourceNames(sources: CtSource[]) {110 for (const s of sources) {111 CT_SOURCE_NAMES[s.id] = s.name;112 CT_SOURCE_CATS[s.id] = s.category;113 }114}115export function ctSourceName(id: string): string {116 return CT_SOURCE_NAMES[id] ?? id;117}118export function ctSourceCategory(id: string): string {119 return CT_SOURCE_CATS[id] ?? "";120}121122async function get<T>(path: string): Promise<T> {123 const ctrl = new AbortController();124 const timer = setTimeout(() => ctrl.abort(), 20000);125 try {126 const res = await fetch(path, { signal: ctrl.signal });127 if (!res.ok) throw new Error(`API ${res.status} — ${path}`);128 return (await res.json()) as T;129 } finally {130 clearTimeout(timer);131 }132}133134export function fetchCtListings(f: CtFilters, limit?: number, offset?: number) {135 const params = new URLSearchParams();136 for (const [k, v] of Object.entries(f)) if (v) params.set(k, v);137 if (limit != null) params.set("limit", String(limit));138 if (offset) params.set("offset", String(offset));139 return get<{ total: number; count: number; listings: CtListing[] }>(140 `/api/ct/listings?${params}`);141}142143export const fetchCtListing = (uid: string) =>144 get<CtListing>(`/api/ct/listings/${encodeURIComponent(uid)}`);145export const fetchCtContext = (uid: string) =>146 get<CtContext>(`/api/ct/listings/${encodeURIComponent(uid)}/context`);147export const fetchCtFacets = () => get<CtFacets>("/api/ct/facets");148export const fetchCtStats = () => get<CtStats>("/api/ct/stats");149export const fetchCtSources = () =>150 get<{ sources: CtSource[] }>("/api/ct/sources");151152export const fmtNight = (p: number | null, label?: string) =>153 p != null154 ? `${p.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $`155 : label || "Prix sur demande";156