diff --git a/backend/src/Controller/SpotScoreController.php b/backend/src/Controller/SpotScoreController.php index 672b8a4..8ba6982 100644 --- a/backend/src/Controller/SpotScoreController.php +++ b/backend/src/Controller/SpotScoreController.php @@ -20,6 +20,46 @@ class SpotScoreController extends AbstractController private Connection $connection, ) {} + /** + * GET /api/spots/overview + * + * Retourne tous les spots avec leur dernier score en UNE seule requête. + * Remplace les N appels individuels à /api/spots/{id}/score pour le rendu + * initial de la carte. + * + * Cache HTTP 30 min (scores mis à jour au max 1-2×/jour par ingestion). + */ + #[Route('/overview', name: 'overview', methods: ['GET'])] + public function overview(): JsonResponse + { + $rows = $this->connection->fetchAllAssociative( + "SELECT + cp.id::text, + cp.name, + cp.type, + cp.region, + ST_X(cp.geometry::geometry) AS longitude, + ST_Y(cp.geometry::geometry) AS latitude, + ist.score, + ist.level, + ROUND(CAST(ist.distance_to_nearest_sargassum AS numeric), 2) AS distance_km + FROM coastal_point cp + LEFT JOIN LATERAL ( + SELECT score, level, distance_to_nearest_sargassum + FROM impact_score + WHERE coastal_point_id = cp.id + ORDER BY timestamp DESC + LIMIT 1 + ) ist ON true + ORDER BY cp.name" + ); + + $response = $this->json($rows); + $response->headers->set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=300'); + + return $response; + } + /** * GET /api/spots/{id}/score * diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 09d4a03..1bc8086 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -25,9 +25,9 @@ async function post(path, body) { export const api = { spots: { - list: () => get('/spots'), - get: (id) => get(`/spots/${id}`), - score: (id, at) => get(`/spots/${id}/score`, { at }), + overview: () => get('/spots/overview'), // tous les spots + score en 1 requête cachée + get: (id) => get(`/spots/${id}`), + score: (id, at) => get(`/spots/${id}/score`, { at }), }, observations: { list: (bbox, date, source) => get('/observations', { bbox, date, source }), diff --git a/frontend/src/components/Map/SargassesMap.jsx b/frontend/src/components/Map/SargassesMap.jsx index 2af2249..8b6514c 100644 --- a/frontend/src/components/Map/SargassesMap.jsx +++ b/frontend/src/components/Map/SargassesMap.jsx @@ -179,8 +179,9 @@ function SpotMarker({ level, scoreValue, scoreDist, isSelected, name }) { export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, flyToTarget }) { const mapRef = useRef(null); + // spots : [{ id, name, type, region, longitude, latitude, score, level, distance_km }] const [spots, setSpots] = useState([]); - const [spotScores, setSpotScores] = useState({}); + const [spotScores, setSpotScores] = useState({}); // id → { level, value, distance } const [viewState, setViewState] = useState(INITIAL_VIEW); const [layerOpacity, setLayerOpacity] = useState(1); const [mapStyle, setMapStyle] = useState(null); // null = style not yet ready @@ -212,28 +213,28 @@ export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, return () => ctrl.abort(); }, []); - // Load coastal spots once + // Charge tous les spots + scores en UNE requête (cachée 30 min côté serveur) useEffect(() => { - api.spots.list() - .then(data => setSpots(Array.isArray(data) ? data : (data['hydra:member'] ?? data.member ?? []))) + api.spots.overview() + .then(rows => { + if (!Array.isArray(rows)) return; + setSpots(rows); + // Pré-remplir spotScores depuis la même réponse — aucune requête supplémentaire + const scores = {}; + rows.forEach(r => { + if (r.level) { + scores[r.id] = { + level: r.level, + value: r.score ?? 0, + distance: r.distance_km ?? null, + }; + } + }); + setSpotScores(scores); + }) .catch(console.error); }, []); - // Load scores progressively for marker colors - useEffect(() => { - if (!spots.length) return; - spots.forEach(s => { - api.spots.score(s.id) - .then(d => { - const level = d?.score?.level; - const value = d?.score?.value ?? 0; - const distance = d?.score?.distance ?? null; - if (level) setSpotScores(prev => ({ ...prev, [s.id]: { level, value, distance } })); - }) - .catch(() => {}); - }); - }, [spots]); - // Fade transition on step change useEffect(() => { setLayerOpacity(0);