123 lines
3.4 KiB
TypeScript
123 lines
3.4 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;
|
||
}
|
||
|
||
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
|
||
.filter((f) => !FORBIDDEN_FEATURES.includes(f.name))
|
||
.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
|
||
|
||
return { summary, loading, error };
|
||
}
|