import { useState, useEffect, useCallback, useRef } from 'react'; import type { FeatureMeta, FeatureFilters, HexagonStatsResponse } from '../types'; import { apiUrl, authHeaders, logNonAbortError } from '../lib/api'; interface UseAreaSummaryOptions { stats: HexagonStatsResponse | null; hexagonId: string | null; isPostcode: boolean; filters: FeatureFilters; features: FeatureMeta[]; } interface UseAreaSummaryResult { summary: string; loading: boolean; error: string | null; retry: () => void; } const FORBIDDEN_FEATURES = ['% White', '% Black', '% Asian', '% Mixed', '% Other', 'Environmental risk', 'Collapsible deposits risk', 'Compressible ground risk', 'Landslide risk', 'Running sand risk', 'Shrink-swell risk', 'Soluble rocks risk' ]; export function useAreaSummary({ stats, hexagonId, isPostcode, filters, features, }: UseAreaSummaryOptions): UseAreaSummaryResult { const [summary, setSummary] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const abortRef = useRef(null); const fetchSummary = useCallback(async () => { if (!stats || !hexagonId) return; abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setSummary(''); setLoading(true); setError(null); try { const filterDescriptions: string[] = []; for (const [name, value] of Object.entries(filters)) { const meta = features.find((f) => f.name === name); if (meta?.type === 'enum') { filterDescriptions.push(`${name}: ${(value as string[]).join(', ')}`); } else { const [min, max] = value as [number, number]; filterDescriptions.push(`${name}: ${min}–${max}`); } } const body = { count: stats.count, location: hexagonId, is_postcode: isPostcode, filters: filterDescriptions, numeric_stats: stats.numeric_features.filter(f => !FORBIDDEN_FEATURES.includes(f.name)).map((f) => ({ name: f.name, mean: f.mean, })), enum_stats: stats.enum_features.map((f) => ({ name: f.name, counts: f.counts, })), }; const url = apiUrl('area-summary'); const response = await fetch(url, authHeaders({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal, })); if (!response.ok) { const text = await response.text(); throw new Error(text || `HTTP ${response.status}`); } const json = await response.json(); setSummary(json.summary || ''); setLoading(false); } catch (err) { if (controller.signal.aborted) return; logNonAbortError('area-summary', err); setError(err instanceof Error ? err.message : 'Failed to generate summary'); setLoading(false); } }, [stats, hexagonId, isPostcode, filters, features]); useEffect(() => { fetchSummary(); return () => { abortRef.current?.abort(); }; }, [stats, hexagonId]); // eslint-disable-line react-hooks/exhaustive-deps const retry = useCallback(() => { fetchSummary(); }, [fetchSummary]); return { summary, loading, error, retry }; }