Files
Investidor-V2/resources/js/components/demographic-evidence.tsx
T
2026-11-15 18:56:39 +02:00

337 lines
16 KiB
TypeScript

import EvidenceDetails from './evidence-details';
import DemographicReadingSummary from './demographic-reading-summary';
export type DemographicAnalysis = {
sector?: string;
state: string;
start_year: number;
end_year: number;
limitations: string[];
comparisons: {
year: number;
left: string;
right: string;
left_value: string;
right_value: string;
left_import: string;
right_import: string;
difference: string | null;
explanation: string;
}[];
differences: {
territory_id: string;
dimension: string;
state: string;
values: { import_id: string; value: string; data_kind: string }[];
}[];
territories: {
id: string;
name: string;
level: string;
factors_to_assess: string[];
series: {
import_id: string;
source: string;
version: string;
sha256: string;
file_name: string;
missing_years: number[];
issue_counts: Record<string, number>;
values: {
year: number;
indicator: string;
age_group: string;
residence: string;
value: string;
original_value: string;
unit: string;
data_kind: string;
sheet: string;
cell: string;
validation_status: string;
}[];
}[];
}[];
};
const indicators: Record<string, string> = {
population: 'População',
life_expectancy: 'Esperança de vida',
infant_mortality: 'Mortalidade infantil',
crude_death_rate: 'Mortalidade bruta',
crude_birth_rate: 'Natalidade bruta',
fertility_rate: 'Fecundidade',
population_growth_rate: 'Crescimento populacional',
};
type Value =
DemographicAnalysis['territories'][number]['series'][number]['values'][number];
export function demographicHighlights(values: Value[]): Value[] {
const totals = values.filter(
(v) => v.age_group === 'all' && v.residence === 'all',
);
// Never add age groups, choose between sources or mix observed/projected values.
return totals.filter(
(v) =>
!totals.some(
(other) =>
other.indicator === v.indicator &&
other.unit === v.unit &&
other.data_kind === v.data_kind &&
other.year > v.year,
),
);
}
export default function DemographicEvidence({
evidence,
}: {
evidence: DemographicAnalysis;
}) {
return (
<section className="space-y-4" aria-label="Evidência demográfica">
<p className="text-sm">
Os indicadores demográficos não demonstram procura ou
rentabilidade. Cruze-os com oferta existente, acessos, custos e
procura documentada.
</p>
{evidence.state !== 'available' && (
<p>
Sem indicadores disponíveis para esta selecção. Isto não
significa população inexistente nem ausência de potencial
económico.
</p>
)}
{evidence.territories.map((t) => (
<article
key={t.id}
className="space-y-3 rounded border bg-white p-4 text-slate-900"
>
<h2 className="text-xl font-semibold">
{t.name} —{' '}
{t.level === 'district' ? 'Distrito' : 'Província'}
</h2>
{t.factors_to_assess.map((f) => (
<p key={f}>{f}</p>
))}
{t.series.length === 0 && (
<p>Sem dados no período escolhido.</p>
)}
{t.series.map((s) => (
<section key={s.import_id} className="space-y-3">
<h3 className="font-semibold">{s.source}</h3>
<DemographicReadingSummary
series={s}
evidence={evidence}
/>
{demographicHighlights(s.values).some(
(v) => v.indicator !== 'population',
) && (
<>
<h4 className="font-semibold">
Outros indicadores mais recentes do
período
</h4>
<dl className="grid gap-3 sm:grid-cols-2">
{demographicHighlights(s.values)
.filter(
(v) =>
v.indicator !==
'population',
)
.map((v) => (
<div
className="rounded border p-3"
key={`${v.sheet}:${v.cell}`}
>
<dt>
{indicators[
v.indicator
] ?? v.indicator}{' '}
· {v.year}
</dt>
<dd className="text-lg font-semibold">
{v.original_value}{' '}
{v.unit ===
'unspecified'
? '(unidade não indicada)'
: v.unit}
</dd>
<dd className="text-sm">
{v.data_kind ===
'projected'
? 'Projecção'
: 'Tipo estatístico não especificado'}
</dd>
{v.validation_status !==
'valid' && (
<dd className="text-sm">
Valor com
advertência na
fonte; consulte o
detalhe antes de o
utilizar.
</dd>
)}
</div>
))}
</dl>
</>
)}
{s.missing_years.length > 0 && (
<p>
Anos sem dados: {s.missing_years.join(', ')}
. Não foram preenchidos por estimativa.
</p>
)}
{(Object.keys(s.issue_counts).length > 0 ||
s.values.some(
(v) => v.validation_status !== 'valid',
)) && (
<p>
Esta série contém advertências. Consulte-as
antes de comparar ou utilizar os valores.
</p>
)}
<EvidenceDetails title="Valores por dimensão, fonte e advertências">
<p>
{s.version} · {s.file_name}
</p>
<p className="text-xs break-all">
SHA256: {s.sha256}
</p>
{Object.keys(s.issue_counts).length > 0 && (
<details>
<summary>Advertências da série</summary>
<pre className="overflow-auto text-xs">
{JSON.stringify(
s.issue_counts,
null,
2,
)}
</pre>
</details>
)}
<div className="max-h-96 overflow-auto">
<table className="w-full min-w-[44rem] text-left text-sm whitespace-nowrap">
<caption className="text-left">
Valores originais por ano e dimensão
— versões apresentadas separadamente
</caption>
<thead>
<tr>
{[
'Ano',
'Indicador',
'Idade',
'Residência',
'Valor',
'Unidade',
'Tipo',
'Referência',
].map((h) => (
<th className="p-2" key={h}>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{s.values.map((v) => (
<tr
key={`${v.sheet}:${v.cell}`}
className="border-t"
>
<td className="p-2">
{v.year}
</td>
<td>
{indicators[
v.indicator
] ?? v.indicator}
</td>
<td>
{v.age_group === 'all'
? 'Todas'
: v.age_group}
</td>
<td>
{
{
all: 'Total',
urban: 'Urbana',
rural: 'Rural',
}[v.residence]
}
</td>
<td>{v.original_value}</td>
<td>
{v.unit ===
'unspecified'
? 'Não indicada na fonte'
: v.unit}
</td>
<td>
{v.data_kind ===
'projected'
? 'Projecção'
: 'Não especificado'}
</td>
<td>
{v.sheet}!{v.cell}
</td>
</tr>
))}
</tbody>
</table>
</div>
</EvidenceDetails>
</section>
))}
</article>
))}
{evidence.comparisons.length > 0 && (
<section className="space-y-3">
<h2 className="text-xl font-semibold">
Diferenças entre localizações
</h2>
{evidence.comparisons.map((c) => (
<p
key={`${c.year}:${c.left_import}:${c.right_import}:${c.left}:${c.right}`}
>
{c.year}: {c.left} ({c.left_value}) / {c.right} (
{c.right_value}). Diferença:{' '}
{c.difference ?? 'não comparável'}. {c.explanation}
</p>
))}
</section>
)}
{evidence.differences.length > 0 && (
<details>
<summary>
Reconciliação entre exemplares:{' '}
{evidence.differences.length} dimensões comuns
</summary>
<ul>
{evidence.differences.map((d) => (
<li key={`${d.territory_id}:${d.dimension}`}>
{d.dimension}:{' '}
{d.state === 'matching_values'
? 'valores coincidentes; conservar ambas as fontes'
: 'valores divergentes'}{' '}
—{' '}
{d.values
.map((v) => `${v.value} (${v.data_kind})`)
.join(' / ')}
</li>
))}
</ul>
</details>
)}
<ul className="list-disc pl-5 text-sm">
{evidence.limitations.map((l) => (
<li key={l}>{l}</li>
))}
</ul>
</section>
);
}