110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
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<string | null>(null);
|
||
const abortRef = useRef<AbortController | null>(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 };
|
||
}
|