diff --git a/app/Services/InvestmentCatalogue.php b/app/Services/InvestmentCatalogue.php index 31ae768f..d6a6e860 100644 --- a/app/Services/InvestmentCatalogue.php +++ b/app/Services/InvestmentCatalogue.php @@ -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]; } } diff --git a/docs/investmoz/consolidated-review-matrix-2026-10-03.md b/docs/investmoz/consolidated-review-matrix-2026-10-03.md index 7c406d51..04884677 100644 --- a/docs/investmoz/consolidated-review-matrix-2026-10-03.md +++ b/docs/investmoz/consolidated-review-matrix-2026-10-03.md @@ -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. diff --git a/docs/investmoz/unification/thematic-map-local-20261104.json b/docs/investmoz/unification/thematic-map-local-20261104.json new file mode 100644 index 00000000..2bfcfc3f --- /dev/null +++ b/docs/investmoz/unification/thematic-map-local-20261104.json @@ -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 + } + } +} diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index b3953795..2fdf80ab 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -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. diff --git a/docs/investmoz/v2/auditoria-portal/backlog.md b/docs/investmoz/v2/auditoria-portal/backlog.md index ebd8a6ed..05d8b0c9 100644 --- a/docs/investmoz/v2/auditoria-portal/backlog.md +++ b/docs/investmoz/v2/auditoria-portal/backlog.md @@ -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. diff --git a/resources/js/components/interactive-investment-map.tsx b/resources/js/components/interactive-investment-map.tsx index bbba0cec..cbdbce4f 100644 --- a/resources/js/components/interactive-investment-map.tsx +++ b/resources/js/components/interactive-investment-map.tsx @@ -24,6 +24,7 @@ export type InvestmentMapFeature = { }; }; export type InvestmentMapArea = { + opportunity_id?: number; selected_territory?: boolean; child_territory?: boolean; location_type?: string; diff --git a/resources/js/components/thematic-investment-map.tsx b/resources/js/components/thematic-investment-map.tsx new file mode 100644 index 00000000..568b85bd --- /dev/null +++ b/resources/js/components/thematic-investment-map.tsx @@ -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 ( +
+
+ + +

+ 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. +

+

+ 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. +

+ {truncated && ( +

+ A resposta está limitada; a cobertura temática refere-se + apenas às fichas recebidas. +

+ )} + {points === null ? ( +

A carregar evidências temáticas…

+ ) : ( + <> +

+ {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. +

+ {selected.length === 0 && ( +

+ 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. +

+ )} + {selected.length > 0 && ( +
+ + Evidências e fontes da camada ( + {countLabel(selected.length)}) + + {selected.map((record) => ( +
+ + {record.title} —{' '} + {located.has(record.id) + ? 'Com ponto da oportunidade' + : 'Sem ponto público utilizável'} + + + Consultar ficha + +

+ Localização da ficha:{' '} + {value(record.location)}. O âmbito + de cada evidência está indicado + abaixo. +

+

+ Situação publicada:{' '} + {record.state === 'confirmed' + ? 'Confirmada no âmbito da síntese publicada' + : 'Hipótese'} + . Limitações:{' '} + {value(record.limitations)} +

+
    + {record.evidence.map((proof) => ( +
  • +

    + {labels[proof.category]} +

    +

    + {value(proof.statement)} +

    +

    + Fonte:{' '} + {value(proof.source)} · + Localizador:{' '} + {value(proof.locator)} +

    +

    + Período:{' '} + {value(proof.period)} · + Verificação registada:{' '} + {value( + proof.verified_on, + )} +

    +

    + Âmbito:{' '} + {value(proof.scope)} · + Cobertura e limites:{' '} + {value(proof.coverage)} +

    +
  • + ))} +
+
+ ))} +
+ )} + + )} +
+ +
+ ); +} diff --git a/resources/js/pages/investment/catalogue.tsx b/resources/js/pages/investment/catalogue.tsx index c7b319f4..cd046dd0 100644 --- a/resources/js/pages/investment/catalogue.tsx +++ b/resources/js/pages/investment/catalogue.tsx @@ -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(null); + const [thematicRecords, setThematicRecords] = useState( + [], + ); const [areas, setAreas] = useState([]); 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.` )} )} @@ -647,6 +654,8 @@ export default function Catalogue({ 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); + } +} diff --git a/tests/frontend/thematic-investment-map.test.mjs b/tests/frontend/thematic-investment-map.test.mjs new file mode 100644 index 00000000..481b5fd0 --- /dev/null +++ b/tests/frontend/thematic-investment-map.test.mjs @@ -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: '', + 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, /