feat: add public resource and infrastructure evidence layers to catalogue map
This commit is contained in:
@@ -522,11 +522,11 @@ class InvestmentCatalogue
|
||||
->whereIn('t.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->select($foreign))
|
||||
->get(['l.opportunity_id', 't.name'])->groupBy('opportunity_id');
|
||||
}
|
||||
$features = $missing = [];
|
||||
$features = $missing = $thematic = [];
|
||||
$areas = (clone $spatial)->whereIn('shape.geometry_type', ['polygon', 'multipolygon'])->orderBy('shape.id')
|
||||
->get(['o.id', 'o.title', 'o.slug', 'g.name', 'shape.source_system', 'shape.properties', DB::raw('ST_AsGeoJSON(shape.geom) as geometry'),
|
||||
DB::raw("CASE WHEN o.primary_territory_id=g.id OR EXISTS (SELECT 1 FROM opportunity_locations direct WHERE direct.opportunity_id=o.id AND direct.geo_location_id=g.id) THEN 'território declarado da oportunidade' ELSE 'contexto do projeto associado; implantação da oportunidade por confirmar' END AS basis")])
|
||||
->map(fn ($area) => ['geometry' => json_decode($area->geometry, true), 'title' => $area->title.' — '.$area->name.' ('.$area->basis.')',
|
||||
->map(fn ($area) => ['opportunity_id' => (int) $area->id, 'geometry' => json_decode($area->geometry, true), 'title' => $area->title.' — '.$area->name.' ('.$area->basis.')',
|
||||
'source_reference' => $area->source_system, 'source_date' => json_decode($area->properties ?? '{}', true)['source_date'] ?? null,
|
||||
'url' => '/oportunidades/'.$area->slug.($filters === [] ? '' : '?'.http_build_query($filters))])->all();
|
||||
$located = 0;
|
||||
@@ -536,6 +536,14 @@ class InvestmentCatalogue
|
||||
foreach ($relations as $relation => $groups) {
|
||||
$properties[$relation] = ($groups->get($record->id) ?? collect())->pluck('name')->unique()->values()->all();
|
||||
}
|
||||
$public = app(ProactiveInvestment::class)->publicData($record);
|
||||
$evidence = array_values(array_filter(StructuredData::collectionItems($public['evidence'] ?? []),
|
||||
fn ($proof) => in_array($proof['category'] ?? null, ['resource', 'infrastructure'], true) && ($proof['purpose'] ?? null) === 'local'));
|
||||
if ($evidence !== []) {
|
||||
$thematic[] = ['id' => $record->id, 'title' => $record->title, 'url' => $properties['url'], 'location' => $properties['location'],
|
||||
'state' => $public['state'] ?? 'hypothesis', 'limitations' => $public['limitations'] ?? null,
|
||||
'evidence' => array_map(fn ($proof) => array_intersect_key($proof, array_flip(['id', 'category', 'statement', 'source', 'locator', 'period', 'scope', 'coverage', 'verified_on'])), $evidence)];
|
||||
}
|
||||
$sites = collect($points->get($record->id) ?? []);
|
||||
if ($sites->isNotEmpty()) {
|
||||
$located++;
|
||||
@@ -552,6 +560,6 @@ class InvestmentCatalogue
|
||||
|
||||
return ['type' => 'FeatureCollection', 'limit' => 1000, 'matched_count' => $total, 'truncated' => $total > $records->count(),
|
||||
'georeferenced_count' => $located, 'marker_count' => count($features), 'unlocated' => $missing, 'features' => $features,
|
||||
'areas' => $areas, 'complete' => $total === $located];
|
||||
'thematic_records' => $thematic, 'areas' => $areas, 'complete' => $total === $located];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2228,3 +2228,18 @@ Delta aprovado: 11 testes de base de dados/125 asserções (cinco novos e seis d
|
||||
Conferência visual no Edge, exclusivamente QA `127.0.0.2:18644`/`main_visual_20261104_test`: Maputo Província mostra zero limites de oito referências distritais acessíveis, lacuna explicitada, nove oportunidades mantidas sem ponto inventado. O snapshot não contém limites públicos para demonstrar visualmente os polígonos inferiores; estes foram exercitados por testes sintéticos. Recibo/hashes: `docs/investmoz/unification/map-subdivision-local-20261104.json`; captura em `outputs/map-subdivision-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Camadas factuais de recursos, infra-estruturas, mão-de-obra e potencial económico, cobertura de dados/geometrias e conferência no portal de destino continuam pendentes. Este recorte não encerra V2-07/V2-10 nem substitui aceitação da equipa. Sem push, migração, escrita activa, importação, alteração de permissões ou reinício de serviços.
|
||||
|
||||
|
||||
### 04/11/2026 — ACT-20261029-02: camadas de evidência pública de recursos e infra-estruturas
|
||||
|
||||
Incremento sequencial na main local sobre `bbc05f1d`, reutilizando V2-07/V2-10, mapa único, filtros, pontos/áreas públicos e contexto administrativo já demonstrados. Inventário de código e cópia QA antes de fixtures: 48 oportunidades publicadas e duas sínteses proactivas públicas, mas nenhuma evidência pública local das categorias resource/infrastructure. Os campos existentes de zonas e fontes editoriais não foram convertidos automaticamente em disponibilidade ou geometrias temáticas; evidências privadas não foram publicadas. Não é perda confirmada de dados nem diagnóstico do remoto. Conferidos por SHA256 os 23 artefactos dos dois recibos visuais/administrativos anteriores, todos inalterados.
|
||||
|
||||
A API existente de mapa acrescenta `thematic_records` a partir de `ProactiveInvestment::publicData`: somente evidência seleccionada na síntese publicada, categorias recursos/infra-estruturas e propósito local. Conserva facto, fonte, localizador, período, âmbito, cobertura, verificação registada, situação e limitações. Exclui metodologia e outras categorias; não copia o rascunho privado. Mantém a consulta territorial, limite/truncagem e autorização pública anteriores. As áreas acrescentam o identificador da oportunidade para filtragem, sem atribuir a geometria à evidência.
|
||||
|
||||
No mesmo mapa, o selector permite todas as oportunidades, recursos documentados ou infra-estruturas documentadas. A camada filtra apenas pontos/áreas existentes das fichas correspondentes; não alarga a pesquisa geral, não altera CSV nem cria posições. Fichas sem ponto continuam consultáveis no painel de evidências/fontes. Contagens referem fichas, não quantidade/capacidade de recursos. Âmbito da evidência distingue-se da localização da oportunidade; marcadores não representam posição exacta ou disponibilidade de recurso/infra-estrutura. Escolha recuperada na sessão do navegador, com tolerância a armazenamento indisponível/inválido. Falta de evidência, período ou fonte é explícita, sem zero presumido.
|
||||
|
||||
Delta aprovado: oito testes de BD/438 asserções (quatro novos e quatro de filtros provinciais), 14 testes frontend (cinco novos e nove existentes), PHPStan sem erros, tipos, Pint, lint e build QA aprovados. Inclui exclusão de rascunhos/publicação futura/acesso restrito, leitura apenas da síntese pública, fonte/período/limites, província/distrito, reutilização das coordenadas existentes, alternância/reposição de camadas, fichas sem ponto, carregamento/ausência/truncagem, escape de texto e recuperação da escolha. Provas gerais anteriores preservadas, sem repetir a bateria completa.
|
||||
|
||||
Conferência visual no Edge exclusivamente em `127.0.0.2:18644`, base `main_visual_20261104_test`: duas fixtures claramente sintéticas criadas apenas em QA, uma por categoria; nenhuma coordenada/geometria criada. Hash de todas as oportunidades anteriores idêntico antes/depois. Alternância entre ambas conserva pesquisa provincial e duas fichas gerais; cada camada mostra a sua ficha, fonte/período/âmbito/limites e ausência de ponto. Infra-estruturas recuperada após recarregar. Recibo: `docs/investmoz/unification/thematic-map-local-20261104.json`; inventário, hashes, DOM e capturas em `outputs/thematic-map-20261104/`. Demonstração sintética não constitui cobertura económica factual ou publicação de dados reais.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual de recursos/infra-estruturas no destino, georreferenciação própria quando documentada, mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e lacunas cartográficas continuam pendentes. Não se inferem estas camadas de descrições nem do número de oportunidades. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, alteração activa, migração, publicação automática ou alteração de permissões/serviços.
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"date": "2026-11-04",
|
||||
"base_commit": "bbc05f1d",
|
||||
"requirement": "ACT-20261029-02; V2-07/V2-10",
|
||||
"scope": "Camadas de evidência temática pública de recursos e infra-estruturas no catálogo existente; main local",
|
||||
"inventory_before_fixtures": {
|
||||
"database": "main_visual_20261104_test",
|
||||
"published_opportunities": 48,
|
||||
"public_proactive_summaries": 2,
|
||||
"public_local_evidence": {
|
||||
"resource": 0,
|
||||
"infrastructure": 0
|
||||
},
|
||||
"methodology_evidence_not_mapped": 0,
|
||||
"sources": [],
|
||||
"other_existing_sources": {
|
||||
"investment_zones.resources/source_evidence": "Textual scope; not converted to georeferenced resource availability",
|
||||
"portal_resources/legacy_content": "Editorial and document sources; no automatic thematic geometry",
|
||||
"private proactive evidence": "Not consulted or published by this increment",
|
||||
"administrative boundaries": "Existing separate layer retained"
|
||||
}
|
||||
},
|
||||
"reuse": [
|
||||
"ProactiveInvestment::publicData: síntese já publicada/seleccionada, sem ler rascunho privado para divulgação.",
|
||||
"Categorias resource/infrastructure com propósito local; metodologia e outras categorias excluídas.",
|
||||
"InvestmentCatalogue::query e map: mesmos filtros, geometrias públicas e limite de 1000 oportunidades.",
|
||||
"Mapa único e contexto administrativo do incremento bbc05f1d preservados."
|
||||
],
|
||||
"contract": {
|
||||
"route": "GET /api/v1/investment/map",
|
||||
"new_field": "thematic_records; id/title/url/location/state/limitations/evidence",
|
||||
"evidence_fields": [
|
||||
"id",
|
||||
"category",
|
||||
"statement",
|
||||
"source",
|
||||
"locator",
|
||||
"period",
|
||||
"scope",
|
||||
"coverage",
|
||||
"verified_on"
|
||||
],
|
||||
"area_field": "opportunity_id para filtrar áreas da mesma oportunidade sem atribuir geometria à evidência"
|
||||
},
|
||||
"behaviour": [
|
||||
"Todas as oportunidades conserva os pontos/áreas anteriores.",
|
||||
"Seleccionar uma categoria apresenta apenas locais/áreas das oportunidades com evidência temática pública local; sem criar posições.",
|
||||
"Fichas sem ponto continuam consultáveis com as evidências e fontes; ausência não equivale a inexistência ou zero.",
|
||||
"Escolha da camada recuperada na sessão do navegador; falha/valor inválido de armazenamento não bloqueia mapa.",
|
||||
"Contagens gerais da pesquisa distinguem-se do subconjunto da camada; CSV e resultados gerais mantêm filtros anteriores.",
|
||||
"Sem inferir mão-de-obra ou potencial económico de descrições ou contagens."
|
||||
],
|
||||
"checks": {
|
||||
"database_tests": 8,
|
||||
"database_assertions": 438,
|
||||
"new_database_tests": 4,
|
||||
"existing_province_regression_tests": 4,
|
||||
"frontend_tests": 14,
|
||||
"new_frontend_tests": 5,
|
||||
"phpstan_errors": 0,
|
||||
"types": "passed",
|
||||
"pint": "passed",
|
||||
"lint": "passed",
|
||||
"qa_build": "passed; avisos de fonte opcional e dimensão de chunks"
|
||||
},
|
||||
"visual": {
|
||||
"target": "http://127.0.0.2:18644",
|
||||
"database": "main_visual_20261104_test",
|
||||
"data": "Duas fixtures exclusivamente sintéticas; base QA sem evidência temática local publicada antes das fixtures.",
|
||||
"resource_selected": true,
|
||||
"infrastructure_selected": true,
|
||||
"infrastructure_selection_recovered_after_reload": true,
|
||||
"source_period_scope_limits_visible": true,
|
||||
"points_created": false,
|
||||
"geometry_created": false,
|
||||
"existing_opportunities_unchanged_sha256": "c9d156f8a71d9196b4f36c3558b830267232b2d407c7abcf9b92bc027bce99d3"
|
||||
},
|
||||
"preservation": {
|
||||
"prior_artifacts_verified_unchanged": 23,
|
||||
"accepted_database_suite_tests_preserved_not_reexecuted": 1325,
|
||||
"prior_records_and_pending_items_retained": true
|
||||
},
|
||||
"pending": [
|
||||
"Cobertura factual real de recursos e infra-estruturas depende de evidências públicas locais existentes no destino; nada foi publicado automaticamente.",
|
||||
"Posição exacta, capacidade e disponibilidade dos recursos/infra-estruturas não foram deduzidas da posição da oportunidade.",
|
||||
"Mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e cobertura cartográfica continuam pendentes.",
|
||||
"Sem conferência remota ou encerramento de ACT-02/V2-07/V2-10."
|
||||
],
|
||||
"safety": {
|
||||
"branch": "main",
|
||||
"push": false,
|
||||
"active_database_changed": false,
|
||||
"schema_or_permissions_changed": false,
|
||||
"automatic_publication": false,
|
||||
"other_services_restarted": false
|
||||
},
|
||||
"proofs": {
|
||||
"outputs/thematic-map-20261104/source-inventory-before-fixtures.json": {
|
||||
"sha256": "7366d737a5cd516c9667a43d62909d1529928285f4da7197c8f0bddab7f85503",
|
||||
"bytes": 700
|
||||
},
|
||||
"outputs/thematic-map-20261104/fixture-receipt.json": {
|
||||
"sha256": "dcf66ccf5773a694638e86cde14bc40e7d2fec56867d6146ac8c9077fc03659d",
|
||||
"bytes": 417
|
||||
},
|
||||
"outputs/thematic-map-20261104/resources.jpg": {
|
||||
"sha256": "f7c3d03e09728d320662a648b083b96ad12bc5bc40825f22529c2d9c882c164f",
|
||||
"bytes": 162214
|
||||
},
|
||||
"outputs/thematic-map-20261104/resources.dom.txt": {
|
||||
"sha256": "6816332137f2fc2a0c840e213997e9f64589166e3ce0e6ebd1619b27bbde5de9",
|
||||
"bytes": 14232
|
||||
},
|
||||
"outputs/thematic-map-20261104/infrastructure-reopened.jpg": {
|
||||
"sha256": "f97455f86832e72eaffd04ce01f53d0a339b966d658c68ba62cfc1d5eb4bc5ba",
|
||||
"bytes": 153964
|
||||
},
|
||||
"outputs/thematic-map-20261104/infrastructure-reopened.dom.txt": {
|
||||
"sha256": "c5fc7b92df9a69c1026a0f7e61217ce96ac5f27b6e3b15d56a2a53a3c3ffb15f",
|
||||
"bytes": 14254
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-delta.log": {
|
||||
"sha256": "ae49afa4a4f8af69ad3ae88470da9d556322b8b6b57b035e2e6476a24d287b1a",
|
||||
"bytes": 96
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-delta.junit.xml": {
|
||||
"sha256": "cc3e177001c7c254832c4eebfa8e13cf5b9a3067121f1fb1efe517fe4d4b8d6e",
|
||||
"bytes": 3756
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-frontend.log": {
|
||||
"sha256": "5523fa5f62f0d40b0ada1ea0faf9d90e649c797dd1b6df89b4fa7086b7e4a6bd",
|
||||
"bytes": 1970
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-phpstan.log": {
|
||||
"sha256": "1997495ca8f04c2ec09798d55607d1a2f85e3338b772f258666465bf658032d2",
|
||||
"bytes": 49
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-types.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-lint.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"outputs/main-db-qa-20261104/thematic-map-build.log": {
|
||||
"sha256": "2f452020e952def0eb1fd9c4469880f35016b23d3757054d84520a5d5d4ef1a1",
|
||||
"bytes": 31606
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9124,3 +9124,18 @@ Delta aprovado: 11 testes de base de dados/125 asserções (cinco novos e seis d
|
||||
Conferência visual no Edge, exclusivamente QA `127.0.0.2:18644`/`main_visual_20261104_test`: Maputo Província mostra zero limites de oito referências distritais acessíveis, lacuna explicitada, nove oportunidades mantidas sem ponto inventado. O snapshot não contém limites públicos para demonstrar visualmente os polígonos inferiores; estes foram exercitados por testes sintéticos. Recibo/hashes: `docs/investmoz/unification/map-subdivision-local-20261104.json`; captura em `outputs/map-subdivision-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Camadas factuais de recursos, infra-estruturas, mão-de-obra e potencial económico, cobertura de dados/geometrias e conferência no portal de destino continuam pendentes. Este recorte não encerra V2-07/V2-10 nem substitui aceitação da equipa. Sem push, migração, escrita activa, importação, alteração de permissões ou reinício de serviços.
|
||||
|
||||
|
||||
### 04/11/2026 — ACT-20261029-02: camadas de evidência pública de recursos e infra-estruturas
|
||||
|
||||
Incremento sequencial na main local sobre `bbc05f1d`, reutilizando V2-07/V2-10, mapa único, filtros, pontos/áreas públicos e contexto administrativo já demonstrados. Inventário de código e cópia QA antes de fixtures: 48 oportunidades publicadas e duas sínteses proactivas públicas, mas nenhuma evidência pública local das categorias resource/infrastructure. Os campos existentes de zonas e fontes editoriais não foram convertidos automaticamente em disponibilidade ou geometrias temáticas; evidências privadas não foram publicadas. Não é perda confirmada de dados nem diagnóstico do remoto. Conferidos por SHA256 os 23 artefactos dos dois recibos visuais/administrativos anteriores, todos inalterados.
|
||||
|
||||
A API existente de mapa acrescenta `thematic_records` a partir de `ProactiveInvestment::publicData`: somente evidência seleccionada na síntese publicada, categorias recursos/infra-estruturas e propósito local. Conserva facto, fonte, localizador, período, âmbito, cobertura, verificação registada, situação e limitações. Exclui metodologia e outras categorias; não copia o rascunho privado. Mantém a consulta territorial, limite/truncagem e autorização pública anteriores. As áreas acrescentam o identificador da oportunidade para filtragem, sem atribuir a geometria à evidência.
|
||||
|
||||
No mesmo mapa, o selector permite todas as oportunidades, recursos documentados ou infra-estruturas documentadas. A camada filtra apenas pontos/áreas existentes das fichas correspondentes; não alarga a pesquisa geral, não altera CSV nem cria posições. Fichas sem ponto continuam consultáveis no painel de evidências/fontes. Contagens referem fichas, não quantidade/capacidade de recursos. Âmbito da evidência distingue-se da localização da oportunidade; marcadores não representam posição exacta ou disponibilidade de recurso/infra-estrutura. Escolha recuperada na sessão do navegador, com tolerância a armazenamento indisponível/inválido. Falta de evidência, período ou fonte é explícita, sem zero presumido.
|
||||
|
||||
Delta aprovado: oito testes de BD/438 asserções (quatro novos e quatro de filtros provinciais), 14 testes frontend (cinco novos e nove existentes), PHPStan sem erros, tipos, Pint, lint e build QA aprovados. Inclui exclusão de rascunhos/publicação futura/acesso restrito, leitura apenas da síntese pública, fonte/período/limites, província/distrito, reutilização das coordenadas existentes, alternância/reposição de camadas, fichas sem ponto, carregamento/ausência/truncagem, escape de texto e recuperação da escolha. Provas gerais anteriores preservadas, sem repetir a bateria completa.
|
||||
|
||||
Conferência visual no Edge exclusivamente em `127.0.0.2:18644`, base `main_visual_20261104_test`: duas fixtures claramente sintéticas criadas apenas em QA, uma por categoria; nenhuma coordenada/geometria criada. Hash de todas as oportunidades anteriores idêntico antes/depois. Alternância entre ambas conserva pesquisa provincial e duas fichas gerais; cada camada mostra a sua ficha, fonte/período/âmbito/limites e ausência de ponto. Infra-estruturas recuperada após recarregar. Recibo: `docs/investmoz/unification/thematic-map-local-20261104.json`; inventário, hashes, DOM e capturas em `outputs/thematic-map-20261104/`. Demonstração sintética não constitui cobertura económica factual ou publicação de dados reais.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual de recursos/infra-estruturas no destino, georreferenciação própria quando documentada, mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e lacunas cartográficas continuam pendentes. Não se inferem estas camadas de descrições nem do número de oportunidades. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, alteração activa, migração, publicação automática ou alteração de permissões/serviços.
|
||||
|
||||
@@ -3432,3 +3432,18 @@ Delta aprovado: 11 testes de base de dados/125 asserções (cinco novos e seis d
|
||||
Conferência visual no Edge, exclusivamente QA `127.0.0.2:18644`/`main_visual_20261104_test`: Maputo Província mostra zero limites de oito referências distritais acessíveis, lacuna explicitada, nove oportunidades mantidas sem ponto inventado. O snapshot não contém limites públicos para demonstrar visualmente os polígonos inferiores; estes foram exercitados por testes sintéticos. Recibo/hashes: `docs/investmoz/unification/map-subdivision-local-20261104.json`; captura em `outputs/map-subdivision-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Camadas factuais de recursos, infra-estruturas, mão-de-obra e potencial económico, cobertura de dados/geometrias e conferência no portal de destino continuam pendentes. Este recorte não encerra V2-07/V2-10 nem substitui aceitação da equipa. Sem push, migração, escrita activa, importação, alteração de permissões ou reinício de serviços.
|
||||
|
||||
|
||||
### 04/11/2026 — ACT-20261029-02: camadas de evidência pública de recursos e infra-estruturas
|
||||
|
||||
Incremento sequencial na main local sobre `bbc05f1d`, reutilizando V2-07/V2-10, mapa único, filtros, pontos/áreas públicos e contexto administrativo já demonstrados. Inventário de código e cópia QA antes de fixtures: 48 oportunidades publicadas e duas sínteses proactivas públicas, mas nenhuma evidência pública local das categorias resource/infrastructure. Os campos existentes de zonas e fontes editoriais não foram convertidos automaticamente em disponibilidade ou geometrias temáticas; evidências privadas não foram publicadas. Não é perda confirmada de dados nem diagnóstico do remoto. Conferidos por SHA256 os 23 artefactos dos dois recibos visuais/administrativos anteriores, todos inalterados.
|
||||
|
||||
A API existente de mapa acrescenta `thematic_records` a partir de `ProactiveInvestment::publicData`: somente evidência seleccionada na síntese publicada, categorias recursos/infra-estruturas e propósito local. Conserva facto, fonte, localizador, período, âmbito, cobertura, verificação registada, situação e limitações. Exclui metodologia e outras categorias; não copia o rascunho privado. Mantém a consulta territorial, limite/truncagem e autorização pública anteriores. As áreas acrescentam o identificador da oportunidade para filtragem, sem atribuir a geometria à evidência.
|
||||
|
||||
No mesmo mapa, o selector permite todas as oportunidades, recursos documentados ou infra-estruturas documentadas. A camada filtra apenas pontos/áreas existentes das fichas correspondentes; não alarga a pesquisa geral, não altera CSV nem cria posições. Fichas sem ponto continuam consultáveis no painel de evidências/fontes. Contagens referem fichas, não quantidade/capacidade de recursos. Âmbito da evidência distingue-se da localização da oportunidade; marcadores não representam posição exacta ou disponibilidade de recurso/infra-estrutura. Escolha recuperada na sessão do navegador, com tolerância a armazenamento indisponível/inválido. Falta de evidência, período ou fonte é explícita, sem zero presumido.
|
||||
|
||||
Delta aprovado: oito testes de BD/438 asserções (quatro novos e quatro de filtros provinciais), 14 testes frontend (cinco novos e nove existentes), PHPStan sem erros, tipos, Pint, lint e build QA aprovados. Inclui exclusão de rascunhos/publicação futura/acesso restrito, leitura apenas da síntese pública, fonte/período/limites, província/distrito, reutilização das coordenadas existentes, alternância/reposição de camadas, fichas sem ponto, carregamento/ausência/truncagem, escape de texto e recuperação da escolha. Provas gerais anteriores preservadas, sem repetir a bateria completa.
|
||||
|
||||
Conferência visual no Edge exclusivamente em `127.0.0.2:18644`, base `main_visual_20261104_test`: duas fixtures claramente sintéticas criadas apenas em QA, uma por categoria; nenhuma coordenada/geometria criada. Hash de todas as oportunidades anteriores idêntico antes/depois. Alternância entre ambas conserva pesquisa provincial e duas fichas gerais; cada camada mostra a sua ficha, fonte/período/âmbito/limites e ausência de ponto. Infra-estruturas recuperada após recarregar. Recibo: `docs/investmoz/unification/thematic-map-local-20261104.json`; inventário, hashes, DOM e capturas em `outputs/thematic-map-20261104/`. Demonstração sintética não constitui cobertura económica factual ou publicação de dados reais.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual de recursos/infra-estruturas no destino, georreferenciação própria quando documentada, mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e lacunas cartográficas continuam pendentes. Não se inferem estas camadas de descrições nem do número de oportunidades. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, alteração activa, migração, publicação automática ou alteração de permissões/serviços.
|
||||
|
||||
@@ -24,6 +24,7 @@ export type InvestmentMapFeature = {
|
||||
};
|
||||
};
|
||||
export type InvestmentMapArea = {
|
||||
opportunity_id?: number;
|
||||
selected_territory?: boolean;
|
||||
child_territory?: boolean;
|
||||
location_type?: string;
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
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';
|
||||
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',
|
||||
};
|
||||
const value = (text?: string | null) => text?.trim() || 'Não indicado';
|
||||
|
||||
export default function ThematicInvestmentMap({
|
||||
points,
|
||||
areas,
|
||||
records,
|
||||
territoryId,
|
||||
truncated,
|
||||
}: {
|
||||
points: InvestmentMapFeature[] | null;
|
||||
areas: InvestmentMapArea[];
|
||||
records: ThematicMapRecord[];
|
||||
territoryId?: string;
|
||||
truncated: boolean;
|
||||
}) {
|
||||
const [theme, setTheme] = useState('all');
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = window.sessionStorage.getItem(
|
||||
'investmoz-map-theme-v1',
|
||||
);
|
||||
if (saved === 'resource' || saved === 'infrastructure')
|
||||
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,
|
||||
)}
|
||||
)
|
||||
</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.
|
||||
</p>
|
||||
<p>
|
||||
Mão-de-obra e potencial económico: sem camada quantitativa
|
||||
nesta consulta; não são inferidos de descrições ou do número
|
||||
de oportunidades.
|
||||
</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 do recurso.
|
||||
</p>
|
||||
{selected.length === 0 && (
|
||||
<p>
|
||||
Sem evidência pública local de recursos ou
|
||||
infra-estruturas para esta 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>
|
||||
<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>
|
||||
<CatalogueInvestmentMap
|
||||
points={visiblePoints}
|
||||
areas={visibleAreas}
|
||||
territoryId={territoryId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,9 @@ import OpportunityRelatedResults, {
|
||||
} from '@/components/opportunity-related-results';
|
||||
import { Link, router, useForm, usePage } from '@inertiajs/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Map from '@/components/catalogue-investment-map';
|
||||
import Map, {
|
||||
type ThematicMapRecord,
|
||||
} from '@/components/thematic-investment-map';
|
||||
import type { InvestmentMapArea } from '@/components/interactive-investment-map';
|
||||
import { type TerritoryOption } from '@/components/investment-territory';
|
||||
import { Frame, button, card, money, api } from './shared';
|
||||
@@ -178,6 +180,9 @@ export default function Catalogue({
|
||||
});
|
||||
}, [comparison, view]);
|
||||
const [points, setPoints] = useState<Feature[] | null>(null);
|
||||
const [thematicRecords, setThematicRecords] = useState<ThematicMapRecord[]>(
|
||||
[],
|
||||
);
|
||||
const [areas, setAreas] = useState<InvestmentMapArea[]>([]);
|
||||
const [unlocated, setUnlocated] = useState<
|
||||
{ id: number; title: string; location: string; url: string }[]
|
||||
@@ -320,6 +325,7 @@ export default function Catalogue({
|
||||
}
|
||||
setPoints(null);
|
||||
setAreas([]);
|
||||
setThematicRecords([]);
|
||||
setUnlocated([]);
|
||||
setMapContext([]);
|
||||
setTruncated(false);
|
||||
@@ -329,6 +335,7 @@ export default function Catalogue({
|
||||
if (active) {
|
||||
setPoints(data.features);
|
||||
setAreas(data.areas ?? []);
|
||||
setThematicRecords(data.thematic_records ?? []);
|
||||
setUnlocated(data.unlocated ?? []);
|
||||
setMapContext(data.territorial_context ?? []);
|
||||
setTruncated(Boolean(data.truncated));
|
||||
@@ -637,7 +644,7 @@ export default function Catalogue({
|
||||
) : points === null ? (
|
||||
'A carregar o mapa…'
|
||||
) : (
|
||||
`${points.length} ${points.length === 1 ? 'local' : 'locais'} no mapa; ${unlocated.length} ${unlocated.length === 1 ? 'oportunidade' : 'oportunidades'} sem coordenadas confirmadas.`
|
||||
`Na pesquisa: ${points.length} ${points.length === 1 ? 'local com coordenadas' : 'locais com coordenadas'}; ${unlocated.length} ${unlocated.length === 1 ? 'oportunidade' : 'oportunidades'} sem coordenadas confirmadas.`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -647,6 +654,8 @@ export default function Catalogue({
|
||||
<Map
|
||||
points={error ? [] : points}
|
||||
areas={areas}
|
||||
records={thematicRecords}
|
||||
truncated={truncated}
|
||||
territoryId={
|
||||
String(
|
||||
filters.locality_id ||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Support\IsolatedQaDatabase;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvestmentThematicMapTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
|
||||
$this->withoutVite();
|
||||
$this->withoutMiddleware(ThrottleRequests::class);
|
||||
}
|
||||
|
||||
private function proof(string $category = 'resource', string $purpose = 'local'): array
|
||||
{
|
||||
return ['id' => (string) Str::uuid(), 'category' => $category, 'purpose' => $purpose,
|
||||
'statement' => 'SYNTHETIC QA documented observation', 'source' => 'Synthetic public source',
|
||||
'locator' => 'Table QA', 'period' => '2026', 'scope' => 'Synthetic district',
|
||||
'coverage' => 'Partial observation; no availability confirmed', 'verified_on' => '2026-10-01'];
|
||||
}
|
||||
|
||||
private function record(string $prefix, array $proofs, ?string $territory = null): Opportunity
|
||||
{
|
||||
$record = Opportunity::create(['sector_id' => 1, 'reference' => 'QA-'.Str::uuid(), 'slug' => 'qa-'.Str::uuid(),
|
||||
'title' => $prefix.' synthetic thematic map', 'status' => 'published', 'published_at' => now()->subMinute(),
|
||||
'primary_territory_id' => $territory]);
|
||||
$record->forceFill(['workflow_context' => ['promotional_summary' => ['projection' => ['proactive' => [
|
||||
'evidence' => $proofs, 'state' => 'hypothesis', 'limitations' => 'SYNTHETIC QA scope only']]],
|
||||
'private_evidence' => 'PRIVATE-DRAFT-MUST-NOT-LEAK']])->save();
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function url(string $prefix, array $filters = []): string
|
||||
{
|
||||
return '/api/v1/investment/map?'.http_build_query(['q' => $prefix, ...$filters]);
|
||||
}
|
||||
|
||||
public function test_only_selected_public_local_thematic_evidence_is_exposed_with_its_sources_and_limits(): void
|
||||
{
|
||||
$prefix = 'THEME-'.Str::uuid();
|
||||
$record = $this->record($prefix, [$this->proof(), $this->proof('infrastructure'), $this->proof('resource', 'methodology'), $this->proof('demand')]);
|
||||
$before = $record->fresh()->getRawOriginal('workflow_context');
|
||||
$response = $this->getJson($this->url($prefix))->assertOk()->assertJsonPath('matched_count', 1)
|
||||
->assertJsonPath('thematic_records.0.id', $record->id)->assertJsonCount(2, 'thematic_records.0.evidence')
|
||||
->assertJsonPath('thematic_records.0.evidence.0.source', 'Synthetic public source')
|
||||
->assertJsonPath('thematic_records.0.evidence.0.period', '2026')
|
||||
->assertJsonPath('thematic_records.0.evidence.0.scope', 'Synthetic district')
|
||||
->assertJsonPath('thematic_records.0.state', 'hypothesis')->assertJsonCount(0, 'features')->assertJsonCount(1, 'unlocated');
|
||||
$this->assertStringNotContainsString('PRIVATE-DRAFT-MUST-NOT-LEAK', $response->getContent());
|
||||
$this->assertSame($before, $record->fresh()->getRawOriginal('workflow_context'));
|
||||
}
|
||||
|
||||
public function test_unpublished_future_restricted_and_unselected_private_evidence_are_not_disclosed(): void
|
||||
{
|
||||
$prefix = 'THEME-'.Str::uuid();
|
||||
$draft = $this->record($prefix, [$this->proof()]);
|
||||
$draft->update(['status' => 'draft']);
|
||||
$future = $this->record($prefix, [$this->proof()]);
|
||||
$future->update(['published_at' => now()->addDay()]);
|
||||
$restricted = $this->record($prefix, [$this->proof()]);
|
||||
$restricted->forceFill(['owner_user_id' => User::factory()->create()->id])->save();
|
||||
$private = $this->record($prefix, []);
|
||||
$private->forceFill(['workflow_context' => ['private_evidence' => [$this->proof()]]])->save();
|
||||
$this->getJson($this->url($prefix))->assertOk()->assertJsonPath('matched_count', 1)->assertJsonCount(0, 'thematic_records');
|
||||
}
|
||||
|
||||
public function test_thematic_records_follow_the_same_province_and_district_filters_without_inventing_coordinates(): void
|
||||
{
|
||||
$prefix = 'THEME-'.Str::uuid();
|
||||
$geo = app(InvestmentGeography::class);
|
||||
$districts = $geo->publicReferences()->where('location_type', 'district')->get(['id', 'parent_id']);
|
||||
$district = $districts->first();
|
||||
$other = $districts->first(fn ($row) => $row->parent_id !== $district->parent_id);
|
||||
$this->assertNotNull($other);
|
||||
$record = $this->record($prefix, [$this->proof()], $district->id);
|
||||
$this->record($prefix, [$this->proof('infrastructure')], $other->id);
|
||||
foreach ([['province_id' => $district->parent_id], ['province_id' => $district->parent_id, 'district_id' => $district->id]] as $filters) {
|
||||
$this->getJson($this->url($prefix, $filters))->assertOk()->assertJsonPath('matched_count', 1)
|
||||
->assertJsonCount(1, 'thematic_records')->assertJsonPath('thematic_records.0.id', $record->id)
|
||||
->assertJsonCount(0, 'features')->assertJsonCount(1, 'unlocated');
|
||||
}
|
||||
$this->assertNull($record->fresh()->district);
|
||||
}
|
||||
|
||||
public function test_thematic_evidence_reuses_existing_site_coordinates_without_relocating_the_resource(): void
|
||||
{
|
||||
$prefix = 'THEME-'.Str::uuid();
|
||||
$record = $this->record($prefix, [$this->proof()]);
|
||||
$site = (string) Str::uuid();
|
||||
DB::table('geo_locations')->insert(['id' => $site, 'name' => 'Synthetic opportunity site', 'code' => $site,
|
||||
'location_type' => 'site', 'data_classification' => 'public', 'source_system' => 'investment-site-editor']);
|
||||
DB::insert("INSERT INTO geo_geometries(id,geo_location_id,geometry_type,geom,properties,data_classification) VALUES (?,?,'point',ST_SetSRID(ST_MakePoint(32.5,-25.9),4326),'{}'::jsonb,'public')", [(string) Str::uuid(), $site]);
|
||||
$record->update(['primary_territory_id' => $site]);
|
||||
$this->getJson($this->url($prefix))->assertOk()->assertJsonCount(1, 'features')
|
||||
->assertJsonPath('features.0.geometry.coordinates', [32.5, -25.9])
|
||||
->assertJsonPath('features.0.properties.id', $record->id)->assertJsonPath('thematic_records.0.id', $record->id);
|
||||
$this->assertSame($site, $record->fresh()->primary_territory_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { compileFunction } from 'node:vm';
|
||||
import { createRequire } from 'node:module';
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import ts from 'typescript';
|
||||
const require = createRequire(import.meta.url);
|
||||
const Map = () => React.createElement('div', { 'data-test-map': true });
|
||||
function setup() {
|
||||
let theme;
|
||||
let mounted = false;
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../resources/js/components/thematic-investment-map.tsx',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const output = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
},
|
||||
}).outputText;
|
||||
const module = { exports: {} };
|
||||
compileFunction(output, ['require', 'module', 'exports'])(
|
||||
(name) =>
|
||||
name === 'react'
|
||||
? {
|
||||
...React,
|
||||
useEffect: (effect) => {
|
||||
if (!mounted) {
|
||||
mounted = true;
|
||||
effect();
|
||||
}
|
||||
},
|
||||
useState: (initial) => {
|
||||
if (theme === undefined)
|
||||
theme =
|
||||
typeof initial === 'function'
|
||||
? initial()
|
||||
: initial;
|
||||
return [
|
||||
theme,
|
||||
(next) => {
|
||||
theme = next;
|
||||
},
|
||||
];
|
||||
},
|
||||
}
|
||||
: name === './catalogue-investment-map'
|
||||
? { default: Map }
|
||||
: require(name),
|
||||
module,
|
||||
module.exports,
|
||||
);
|
||||
return (...args) => {
|
||||
module.exports.default(...args);
|
||||
return module.exports.default(...args);
|
||||
};
|
||||
}
|
||||
const flat = (node) =>
|
||||
!node || typeof node !== 'object'
|
||||
? []
|
||||
: [node, ...React.Children.toArray(node.props.children).flatMap(flat)];
|
||||
const proof = (category) => ({
|
||||
id: category,
|
||||
category,
|
||||
statement: '<script>synthetic</script>',
|
||||
source: 'Synthetic source',
|
||||
locator: 'Table 4',
|
||||
period: '2024',
|
||||
scope: 'District A',
|
||||
coverage: 'Partial coverage',
|
||||
verified_on: '2026-10-01',
|
||||
});
|
||||
const records = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Resource fixture',
|
||||
url: '/fixture/1',
|
||||
location: 'District A',
|
||||
state: 'hypothesis',
|
||||
limitations: 'No availability confirmed',
|
||||
evidence: [proof('resource')],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Infrastructure fixture',
|
||||
url: '/fixture/2',
|
||||
location: 'Province B',
|
||||
state: 'hypothesis',
|
||||
limitations: null,
|
||||
evidence: [proof('infrastructure')],
|
||||
},
|
||||
];
|
||||
const points = [1, 3].map((id) => ({
|
||||
geometry: { coordinates: [32, -25] },
|
||||
properties: {
|
||||
id,
|
||||
title: 'Site ' + id,
|
||||
url: '/fixture/' + id,
|
||||
location: 'Confirmed opportunity site',
|
||||
},
|
||||
}));
|
||||
const areas = [1, 2, 3].map((id) => ({
|
||||
opportunity_id: id,
|
||||
title: 'Area ' + id,
|
||||
geometry: { type: 'Polygon', coordinates: [] },
|
||||
}));
|
||||
const props = {
|
||||
points,
|
||||
areas,
|
||||
records,
|
||||
territoryId: 'province-a',
|
||||
truncated: false,
|
||||
};
|
||||
|
||||
test('default preserves every opportunity and one map, with sourced thematic evidence and no new location claims', () => {
|
||||
const Component = setup(),
|
||||
tree = Component(props),
|
||||
nodes = flat(tree);
|
||||
const map = nodes.find((node) => node.type === Map);
|
||||
assert.equal(nodes.filter((node) => node.type === Map).length, 1);
|
||||
assert.equal(map.props.points, points);
|
||||
assert.equal(map.props.areas, areas);
|
||||
assert.equal(map.props.territoryId, 'province-a');
|
||||
const html = renderToStaticMarkup(tree);
|
||||
for (const text of [
|
||||
'Synthetic source',
|
||||
'Table 4',
|
||||
'2024',
|
||||
'District A',
|
||||
'Partial coverage',
|
||||
'2026-10-01',
|
||||
'não representam a posição exacta',
|
||||
'não são inferidos',
|
||||
])
|
||||
assert.ok(html.includes(text), text);
|
||||
assert.match(html, /2 fichas com evidência temática pública/);
|
||||
assert.match(html, /1 sem ponto público utilizável/);
|
||||
assert.doesNotMatch(html, /<script>/);
|
||||
});
|
||||
|
||||
test('choosing resources filters existing points and opportunity areas, retaining administrative identity and source context', () => {
|
||||
const Component = setup();
|
||||
flat(Component(props))
|
||||
.find((node) => node.type === 'select')
|
||||
.props.onChange({ target: { value: 'resource' } });
|
||||
const tree = Component(props),
|
||||
map = flat(tree).find((node) => node.type === Map);
|
||||
assert.deepEqual(map.props.points, [points[0]]);
|
||||
assert.deepEqual(map.props.areas, [areas[0]]);
|
||||
assert.equal(map.props.territoryId, 'province-a');
|
||||
const html = renderToStaticMarkup(tree);
|
||||
assert.match(html, /Resource fixture/);
|
||||
assert.doesNotMatch(html, /Infrastructure fixture/);
|
||||
flat(tree)
|
||||
.find((node) => node.type === 'select')
|
||||
.props.onChange({ target: { value: 'all' } });
|
||||
assert.equal(
|
||||
flat(Component(props)).find((node) => node.type === Map).props.points,
|
||||
points,
|
||||
);
|
||||
});
|
||||
|
||||
test('infrastructure without confirmed coordinates remains consultable without borrowing another opportunity point', () => {
|
||||
const Component = setup();
|
||||
flat(Component(props))
|
||||
.find((node) => node.type === 'select')
|
||||
.props.onChange({ target: { value: 'infrastructure' } });
|
||||
const tree = Component(props),
|
||||
map = flat(tree).find((node) => node.type === Map);
|
||||
assert.deepEqual(map.props.points, []);
|
||||
assert.deepEqual(map.props.areas, [areas[1]]);
|
||||
assert.match(renderToStaticMarkup(tree), /Infrastructure fixture/);
|
||||
assert.match(renderToStaticMarkup(tree), /Sem ponto público utilizável/);
|
||||
assert.ok(
|
||||
flat(tree).some(
|
||||
(node) => node.type === 'a' && node.props.href === '/fixture/2',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('changing query data never retains previous evidence; loading and incomplete coverage are explicit', () => {
|
||||
const Component = setup();
|
||||
flat(Component(props))
|
||||
.find((node) => node.type === 'select')
|
||||
.props.onChange({ target: { value: 'resource' } });
|
||||
const loading = Component({
|
||||
...props,
|
||||
records: [],
|
||||
points: null,
|
||||
areas: [],
|
||||
});
|
||||
assert.equal(
|
||||
flat(loading).find((node) => node.type === Map).props.points,
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
flat(loading).find((node) => node.type === 'select').props.disabled,
|
||||
true,
|
||||
);
|
||||
assert.doesNotMatch(renderToStaticMarkup(loading), /Resource fixture/);
|
||||
const html = renderToStaticMarkup(
|
||||
Component({
|
||||
...props,
|
||||
points: [],
|
||||
records: [],
|
||||
areas: [],
|
||||
truncated: true,
|
||||
}),
|
||||
);
|
||||
assert.match(html, /Sem evidência pública local/);
|
||||
assert.match(html, /A resposta está limitada/);
|
||||
assert.match(html, /não significa ausência do recurso/);
|
||||
});
|
||||
|
||||
test('theme selection survives reload in the tab and unavailable or invalid storage does not break the map', () => {
|
||||
const previous = globalThis.window;
|
||||
let saved = 'resource';
|
||||
globalThis.window = {
|
||||
sessionStorage: {
|
||||
getItem: () => saved,
|
||||
setItem: (key, value) => {
|
||||
assert.equal(key, 'investmoz-map-theme-v1');
|
||||
saved = value;
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const Component = setup();
|
||||
let select = flat(Component(props)).find(
|
||||
(node) => node.type === 'select',
|
||||
);
|
||||
assert.equal(select.props.value, 'resource');
|
||||
select.props.onChange({ target: { value: 'infrastructure' } });
|
||||
assert.equal(saved, 'infrastructure');
|
||||
assert.equal(
|
||||
flat(setup()(props)).find((node) => node.type === 'select').props
|
||||
.value,
|
||||
'infrastructure',
|
||||
);
|
||||
saved = 'invalid';
|
||||
assert.equal(
|
||||
flat(setup()(props)).find((node) => node.type === 'select').props
|
||||
.value,
|
||||
'all',
|
||||
);
|
||||
globalThis.window = {
|
||||
sessionStorage: {
|
||||
getItem: () => {
|
||||
throw new Error('Storage unavailable');
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error('Storage unavailable');
|
||||
},
|
||||
},
|
||||
};
|
||||
const WithoutStorage = setup();
|
||||
select = flat(WithoutStorage(props)).find(
|
||||
(node) => node.type === 'select',
|
||||
);
|
||||
assert.equal(select.props.value, 'all');
|
||||
select.props.onChange({ target: { value: 'resource' } });
|
||||
assert.equal(
|
||||
flat(WithoutStorage(props)).find((node) => node.type === 'select')
|
||||
.props.value,
|
||||
'resource',
|
||||
);
|
||||
} finally {
|
||||
if (previous === undefined) delete globalThis.window;
|
||||
else globalThis.window = previous;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user