337 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|