SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.4 KB · 156 lines typescript
Raw Blame History
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