Files
Investidor-V2/resources/js/components/investment-map-panel.tsx
T
2026-11-02 20:31:15 +02:00

124 lines
4.6 KiB
TypeScript

import SearchState from '@/components/search-state';
import { useEffect, useState } from 'react';
import TerritorialMap from './catalogue-investment-map';
import InvestmentMapCoverage from './investment-map-coverage';
import {
type InvestmentMapFeature,
type InvestmentMapArea,
} from './interactive-investment-map';
export default function InvestmentMapPanel({
filters,
territoryId,
}: {
filters: Record<string, string | number>;
territoryId?: string;
}) {
const key = JSON.stringify(filters);
const [points, setPoints] = useState<InvestmentMapFeature[] | null>(null);
const [missing, setMissing] = useState<
{ id: number; title: string; url: string }[]
>([]);
const [error, setError] = useState('');
const [retry, setRetry] = useState(0);
const [areas, setAreas] = useState<InvestmentMapArea[]>([]);
const [truncated, setTruncated] = useState(false);
const [matched, setMatched] = useState(0);
const [responseKey, setResponseKey] = useState('');
const current = responseKey === key;
useEffect(() => {
const controller = new AbortController();
const params = new URLSearchParams(JSON.parse(key));
setPoints(null);
setError('');
setMissing([]);
setAreas([]);
setTruncated(false);
setMatched(0);
fetch(`/api/v1/investment/map?${params}`, {
signal: controller.signal,
headers: { Accept: 'application/json' },
})
.then(async (response) => {
if (!response.ok) throw new Error('Mapa indisponível.');
return response.json();
})
.then((data) => {
if (controller.signal.aborted) return;
setPoints(data.features);
setResponseKey(key);
setAreas(data.areas ?? []);
setMissing(data.unlocated ?? []);
setTruncated(Boolean(data.truncated));
setMatched(
data.matched_count ??
new Set(
(data.features as InvestmentMapFeature[]).map(
(point) => point.properties.id,
),
).size + (data.unlocated?.length ?? 0),
);
})
.catch(() => {
if (!controller.signal.aborted) {
setResponseKey(key);
setError('Não foi possível concluir a pesquisa.');
}
});
return () => controller.abort();
}, [key, retry]);
return (
<section className="space-y-3">
<SearchState
scope="Registos consultados pelo mapa"
countLabel="investimentos, incluindo os que não têm coordenadas"
loading={!current || (points === null && !error)}
error={current ? error : ''}
total={current ? matched : 0}
retry={() => setRetry((value) => value + 1)}
/>
<TerritorialMap
points={current ? (error ? [] : points) : null}
areas={current ? areas : []}
territoryId={
territoryId ||
String(
filters.locality_id ||
filters.admin_post_id ||
filters.district_id ||
filters.province_id ||
'',
) ||
undefined
}
/>
{current && points !== null && (
<InvestmentMapCoverage
points={points}
areas={areas}
unlocated={missing.length}
truncated={truncated}
/>
)}
{current && missing.length > 0 && (
<div>
<p>
{missing.length} investimentos sem ponto público
utilizável. Consulte as fichas para localizações
documentadas sem coordenadas:
</p>
<ul>
{missing.map((record) => (
<li key={record.id}>
<a href={record.url} className="underline">
{record.title}
</a>
</li>
))}
</ul>
</div>
)}
</section>
);
}