286 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|