// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/useFicheData.ts : orchestration des données secondaires de la fiche. // Un seul endroit charge les API annexes (au lieu d'un fetch par bloc) : // · groupe CRITIQUE (au montage) : juste valeur détaillée, registre des // loyers, inondation, qualité de l'air, coût réel — nécessaires au score, // à « En bref » et au premier écran ; // · groupe DIFFÉRÉ (quand l'utilisateur approche la carte, ou après 2,5 s) : // commerces/transport, essence, historique, recyclées, TAL, gestionnaire, // Hydro-Québec (cache seulement). // Chaque ressource porte son statut (loading / ok / error / empty) pour des // squelettes et des états vides propres — jamais de undefined à l'écran. // ----------------------------------------------------------------------------- import { useCallback, useEffect, useRef, useState } from "react"; import { AirNearby, CommercesNearby, CoutReel, FairValueDetail, GazNearby, Gestionnaire, HistoriqueLouka, HydroEstimate, Inondation, Listing, RdlNearby, Recyclees, TalHistory, fetchAir, fetchCommerces, fetchCoutReel, fetchFairValue, fetchGaz, fetchGestionnaire, fetchHistorique, fetchHydro, fetchInondation, fetchRdl, fetchRecyclees, fetchTal, } from "../api"; export type Res = | { status: "idle" | "loading" } | { status: "ok"; data: T } | { status: "error"; error: string } | { status: "na" }; // non applicable (pas de coordonnées, pas d'adresse…) export interface FicheData { fv: Res; rdl: Res; inondation: Res; air: Res; cout: Res; commerces: Res; gaz: Res; historique: Res; recyclees: Res; tal: Res; gestionnaire: Res; hydro: Res; /** déclenche le groupe différé (appelé par le sentinelle de la carte) */ wake: () => void; /** relance une ressource en erreur */ retry: (key: keyof Omit) => void; } type Key = keyof Omit; const LOADING = { status: "loading" } as const; const NA = { status: "na" } as const; const CRITIQUE: Key[] = ["fv", "rdl", "inondation", "air", "cout"]; const DIFFERE: Key[] = ["commerces", "gaz", "historique", "recyclees", "tal", "gestionnaire", "hydro"]; export function dataOf(r: Res): T | null { return r.status === "ok" ? r.data : null; } export default function useFicheData(l: Listing | null): FicheData { const [state, setState] = useState>>({ fv: LOADING, rdl: LOADING, inondation: LOADING, air: LOADING, cout: LOADING, commerces: { status: "idle" }, gaz: { status: "idle" }, historique: { status: "idle" }, recyclees: { status: "idle" }, tal: { status: "idle" }, gestionnaire: { status: "idle" }, hydro: { status: "idle" }, }); const [awake, setAwake] = useState(false); const uidRef = useRef(null); const started = useRef>(new Set()); const set = useCallback((k: Key, r: Res) => setState((s) => ({ ...s, [k]: r })), []); const load = useCallback((k: Key, p: (() => Promise) | null) => { if (!p) { set(k, NA); return; } const uid = uidRef.current; set(k, LOADING); p().then((d) => { if (uidRef.current === uid) set(k, { status: "ok", data: d }); }) .catch((e: unknown) => { if (uidRef.current !== uid) return; // 404 = base absente ou hors couverture → état « non applicable » (pas une erreur) const msg = String(e); set(k, /API 404/.test(msg) ? NA : { status: "error", error: msg }); }); }, [set]); const loaders = useCallback((k: Key): (() => Promise) | null => { if (!l) return null; const geo = l.lat != null && l.lng != null; const lat = l.lat as number, lng = l.lng as number; switch (k) { case "fv": return l.price != null ? () => fetchFairValue(l.uid) : null; case "rdl": return geo ? () => fetchRdl(lat, lng, 600, 300) : null; case "inondation": return geo ? () => fetchInondation(lat, lng) : null; case "air": return geo ? () => fetchAir(lat, lng) : null; case "cout": return () => fetchCoutReel(l.uid); case "commerces": return geo ? () => fetchCommerces(lat, lng) : null; case "gaz": return geo ? () => fetchGaz(lat, lng, 60) : null; case "historique": return () => fetchHistorique(l.uid); case "recyclees": return () => fetchRecyclees(l.uid); case "tal": return l.address ? () => fetchTal(l.address, l.city) : null; case "gestionnaire": return () => fetchGestionnaire(l.source); case "hydro": return (l.address || l.title) ? () => fetchHydro(l.address || l.title, { uid: l.uid, lat: l.lat, lng: l.lng }, false) : null; } }, [l]); // groupe critique : dès que l'annonce est connue useEffect(() => { if (!l) return; uidRef.current = l.uid; started.current = new Set(); setAwake(false); for (const k of CRITIQUE) load(k, loaders(k)); for (const k of DIFFERE) set(k, { status: "idle" }); const t = setTimeout(() => setAwake(true), 2500); // filet : réveil après 2,5 s return () => clearTimeout(t); }, [l, load, loaders, set]); // groupe différé : au réveil (proximité de la carte ou délai) useEffect(() => { if (!l || !awake) return; for (const k of DIFFERE) { if (started.current.has(k)) continue; started.current.add(k); load(k, loaders(k)); } }, [awake, l, load, loaders]); const wake = useCallback(() => setAwake(true), []); const retry = useCallback((k: Key) => load(k, loaders(k)), [load, loaders]); return { ...(state as unknown as Omit), wake, retry }; }