Files
Investidor-V2/resources/js/components/thematic-investment-map.tsx
T
2026-11-04 11:45:42 +02:00

286 lines
13 KiB
TypeScript

import { useEffect, useState } from 'react';
import InvestmentMapIndicators, {
type QuantitativeMapContext,
} from './investment-map-indicators';
import CatalogueInvestmentMap from './catalogue-investment-map';
import type {
InvestmentMapArea,
InvestmentMapFeature,
} from './interactive-investment-map';
export type ThematicMapRecord = {
id: number;
title: string;
url: string;
location: string;
state: string;
limitations: string | null;
evidence: {
id: string;
category:
| 'resource'
| 'infrastructure'
| 'workforce'
| 'economic_potential';
source_category?: string;
statement?: string | null;
source?: string | null;
locator?: string | null;
period?: string | null;
scope?: string | null;
coverage?: string | null;
verified_on?: string | null;
}[];
};
const labels = {
resource: 'Recursos documentados',
infrastructure: 'Infra-estruturas documentadas',
workforce: 'Mão-de-obra documentada',
economic_potential: 'Potencial económico — contexto documentado',
};
const sourceLabels: Record<string, string> = {
activity: 'Actividade',
supply: 'Oferta',
demand: 'Procura',
workforce: 'Mão-de-obra',
resource: 'Recurso',
infrastructure: 'Infra-estrutura',
};
const value = (text?: string | null) => text?.trim() || 'Não indicado';
export default function ThematicInvestmentMap({
points,
areas,
records,
territoryId,
truncated,
quantitative = null,
}: {
points: InvestmentMapFeature[] | null;
areas: InvestmentMapArea[];
records: ThematicMapRecord[];
territoryId?: string;
truncated: boolean;
quantitative?: QuantitativeMapContext | null;
}) {
const [theme, setTheme] = useState('all');
useEffect(() => {
try {
const saved = window.sessionStorage.getItem(
'investmoz-map-theme-v1',
);
if (saved && Object.hasOwn(labels, saved)) setTheme(saved);
} catch {
// The server and first client render use the same default layer.
}
}, []);
const selectTheme = (next: string) => {
setTheme(next);
try {
window.sessionStorage.setItem('investmoz-map-theme-v1', next);
} catch {
// Keep the map usable when session storage is unavailable.
}
};
const selected = records
.map((record) => ({
...record,
evidence: record.evidence.filter(
(proof) => theme === 'all' || proof.category === theme,
),
}))
.filter((record) => record.evidence.length > 0);
const ids = new Set(selected.map((record) => record.id));
const visiblePoints =
theme === 'all'
? points
: (points?.filter((point) => ids.has(point.properties.id)) ?? null);
const visibleAreas =
theme === 'all'
? areas
: areas.filter(
(area) =>
area.opportunity_id !== undefined &&
ids.has(area.opportunity_id),
);
const countLabel = (count: number) =>
`${count} ${count === 1 ? 'ficha' : 'fichas'}`;
const located = new Set((points ?? []).map((point) => point.properties.id));
return (
<div className="space-y-3">
<section
className="space-y-3 rounded border p-3"
aria-label="Camadas temáticas documentadas"
>
<label
className="block font-semibold"
htmlFor="investment-map-theme"
>
Camada temática do mapa
</label>
<select
id="investment-map-theme"
className="w-full rounded border p-2"
value={theme}
disabled={points === null}
onChange={(event) => selectTheme(event.target.value)}
>
<option value="all">Todas as oportunidades</option>
{Object.entries(labels).map(([key, label]) => (
<option key={key} value={key}>
{label} (
{countLabel(
records.filter((record) =>
record.evidence.some(
(proof) => proof.category === key,
),
).length,
)}{' '}
de oportunidades)
</option>
))}
</select>
<p>
A camada filtra os locais e áreas das oportunidades com
evidência pública seleccionada. Os marcadores localizam a
oportunidade; não representam a posição exacta, capacidade
ou disponibilidade do recurso ou da infra-estrutura. A
pesquisa geral mantém os seus filtros e resultados. As
contagens desta secção são de oportunidades; os indicadores
territoriais têm contagem própria.
</p>
<p>
Mão-de-obra: a evidência pode descrever necessidades ou
qualificações; não mede trabalhadores disponíveis. Potencial
económico: contexto de actividade, oferta e procura, sem
índice, previsão de retorno ou viabilidade calculados.
Fontes e períodos não são agregados como se fossem
comparáveis.
</p>
{truncated && (
<p role="note">
A resposta está limitada; a cobertura temática refere-se
apenas às fichas recebidas.
</p>
)}
{points === null ? (
<p role="status">A carregar evidências temáticas…</p>
) : (
<>
<p role="status">
{countLabel(selected.length)} com evidência temática
pública nesta resposta;{' '}
{
selected.filter(
(record) => !located.has(record.id),
).length
}{' '}
sem ponto público utilizável. Ausência de evidência
não significa ausência de recursos ou actividade
económica.
</p>
{selected.length === 0 && (
<p>
Sem evidência pública local nas oportunidades
desta camada e estes filtros. Não foram criadas
posições no mapa.
</p>
)}
{selected.length > 0 && (
<details className="space-y-2">
<summary>
Evidências e fontes da camada (
{countLabel(selected.length)})
</summary>
{selected.map((record) => (
<details
key={record.id}
className="rounded border p-2"
>
<summary>
{record.title} —{' '}
{located.has(record.id)
? 'Com ponto da oportunidade'
: 'Sem ponto público utilizável'}
</summary>
<a
className="underline"
href={record.url}
>
Consultar ficha
</a>
<p>
Localização da ficha:{' '}
{value(record.location)}. O âmbito
de cada evidência está indicado
abaixo.
</p>
<p>
Situação publicada:{' '}
{record.state === 'confirmed'
? 'Confirmada no âmbito da síntese publicada'
: 'Hipótese'}
. Limitações:{' '}
{value(record.limitations)}
</p>
<ul>
{record.evidence.map((proof) => (
<li
key={proof.id}
className="my-2 space-y-1 border-t pt-2"
>
<p className="font-semibold">
{labels[proof.category]}
</p>
{proof.source_category && (
<p>
Categoria de origem:{' '}
{sourceLabels[
proof
.source_category
] ?? 'Não indicada'}
</p>
)}
<p>
{value(proof.statement)}
</p>
<p>
Fonte:{' '}
{value(proof.source)} ·
Localizador:{' '}
{value(proof.locator)}
</p>
<p>
Período:{' '}
{value(proof.period)} ·
Verificação registada:{' '}
{value(
proof.verified_on,
)}
</p>
<p>
Âmbito:{' '}
{value(proof.scope)} ·
Cobertura e limites:{' '}
{value(proof.coverage)}
</p>
</li>
))}
</ul>
</details>
))}
</details>
)}
</>
)}
</section>
<InvestmentMapIndicators context={quantitative} theme={theme} />
<CatalogueInvestmentMap
points={visiblePoints}
areas={visibleAreas}
territoryId={territoryId}
/>
</div>
);
}