commit 23
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"date": "2026-10-29",
|
||||
"scope": "Reorganização visual local do catálogo autorizada pela Tempo; sem instalação no portal.",
|
||||
"base": "838ad941a24ae76ec4bf91ccc3f92012668fe8cf",
|
||||
"design": {
|
||||
"desktop": "Painel lateral de filtros e resultados em duas colunas; lista com acções na coluna lateral do cartão.",
|
||||
"mobile": "Filtros expansíveis; cartões numa coluna; áreas de toque e foco visível.",
|
||||
"content": "Todos os campos, descrições, avisos e acções conservados; 29 fichas agrupadas por tipo. Aviso provincial e título dos resultados recuperados do pedido fornecido."
|
||||
},
|
||||
"checks": {
|
||||
"types": {
|
||||
"command": "npm run types:check",
|
||||
"exit": 0
|
||||
},
|
||||
"build": {
|
||||
"command": "INVESTMOZ_QA_BUILD=1 npm run build",
|
||||
"exit": 0,
|
||||
"target": "public/build-qa-opportunities",
|
||||
"limitations": "Avisos existentes de fontaine opcional e chunks grandes. Compilação anterior à formatação final, apenas cosmética."
|
||||
},
|
||||
"browser": {
|
||||
"result": "passed",
|
||||
"scope": "Local compiled React application with public document-based preview fixtures and mocked HTTP, no database or external requests. Does not prove backend or live availability.",
|
||||
"layouts": [
|
||||
{
|
||||
"width": 1440,
|
||||
"scrollWidth": 1440,
|
||||
"count": 12,
|
||||
"headingY": 296.65625,
|
||||
"sidebarVisible": true,
|
||||
"groups": [
|
||||
"Corredores6",
|
||||
"Clusters8",
|
||||
"Projectos âncora15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"scrollWidth": 1024,
|
||||
"count": 12,
|
||||
"headingY": 292.234375,
|
||||
"sidebarVisible": true,
|
||||
"groups": [
|
||||
"Corredores6",
|
||||
"Clusters8",
|
||||
"Projectos âncora15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"scrollWidth": 768,
|
||||
"count": 12,
|
||||
"headingY": 368.078125,
|
||||
"sidebarVisible": false,
|
||||
"groups": [
|
||||
"Corredores6",
|
||||
"Clusters8",
|
||||
"Projectos âncora15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"scrollWidth": 390,
|
||||
"count": 12,
|
||||
"headingY": 395.265625,
|
||||
"sidebarVisible": false,
|
||||
"groups": [
|
||||
"Corredores6",
|
||||
"Clusters8",
|
||||
"Projectos âncora15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"scrollWidth": 320,
|
||||
"count": 12,
|
||||
"headingY": 425.96875,
|
||||
"sidebarVisible": false,
|
||||
"groups": [
|
||||
"Corredores6",
|
||||
"Clusters8",
|
||||
"Projectos âncora15"
|
||||
]
|
||||
}
|
||||
],
|
||||
"checks": [
|
||||
"All 12 sample titles, full descriptions and territorial caveats preserved at every width; expanded filter controls fit 320/390/1024px.",
|
||||
"Missing currency error remains visible on mobile.",
|
||||
"Keyboard opens mobile filters; search preserved through collapse, reopen and reload.",
|
||||
"Hierarchical territory selector still loads and updates filter context (synthetic QA territory).",
|
||||
"Investment selection and dossier link survive reload.",
|
||||
"Comparison still limits four selections and renders the existing comparison table.",
|
||||
"All 29 territorial reference cards remain accessible in three expandable groups.",
|
||||
"All five views activate and fit mobile width; map uses explicit no-coordinates preview data."
|
||||
],
|
||||
"pageErrors": 0
|
||||
},
|
||||
"logic": [
|
||||
{
|
||||
"file": "resources/js/pages/investment/catalogue.tsx",
|
||||
"existingNonPresentationLogicBlocks": 33,
|
||||
"missing": 0,
|
||||
"excludes": "JSX and its inline handlers; those reviewed in source diff and directed browser checks."
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/opportunity-search-filters.tsx",
|
||||
"existingNonPresentationLogicBlocks": 5,
|
||||
"missing": 0,
|
||||
"excludes": "JSX and its inline handlers; those reviewed in source diff and directed browser checks."
|
||||
}
|
||||
]
|
||||
},
|
||||
"preservation": {
|
||||
"schema": "Sem alterações.",
|
||||
"data": "Sem acesso à base de dados; prévia baseada nos documentos locais e referências públicas já guardadas.",
|
||||
"configuration": "Sem alterações.",
|
||||
"permissions": "Sem alterações.",
|
||||
"queries": "Filtros, endpoints e regras preservados; testes de backend anteriores reutilizados, sem nova prova de backend ou autenticação.",
|
||||
"sourceFiles": [
|
||||
{
|
||||
"file": "resources/js/pages/investment/catalogue.tsx",
|
||||
"sha256": "d4697cb83c2f92a786108286c7743401b44d61c5b367e9ff322b05d0424b0d70"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/opportunity-search-filters.tsx",
|
||||
"sha256": "d5b75fc78f7dc272b745117d37e040f1bb921cec2783c3f50ddaaafff7e2df66"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/catalogue-filter-group.tsx",
|
||||
"sha256": "f2728dfc2393de7d4f4338d3ea1057ed84317603980a2dc44f8a208d225cda45"
|
||||
},
|
||||
{
|
||||
"file": "resources/css/investment-catalogue.css",
|
||||
"sha256": "034a79a62ba13220f814027afd62e43c49e2947afd1d9e25a87ba37d3648fd93"
|
||||
}
|
||||
]
|
||||
},
|
||||
"evidence": {
|
||||
"directory": "outputs/catalogue-layout-20261029/",
|
||||
"screenshots": [
|
||||
"catalogue-1440.png",
|
||||
"catalogue-1024.png",
|
||||
"catalogue-768.png",
|
||||
"catalogue-390.png",
|
||||
"catalogue-320.png",
|
||||
"filters-expanded-390.png",
|
||||
"territorial-expanded.png",
|
||||
"list-1440.png"
|
||||
]
|
||||
},
|
||||
"matrix": [
|
||||
"RF-MN-010",
|
||||
"RF-PII-003",
|
||||
"RNF-EDU-008",
|
||||
"MASTER-471012ab-0124"
|
||||
],
|
||||
"deployment": "Não realizada; alteração activa fora desta entrega local.",
|
||||
"dynamicMoreFilters": {
|
||||
"origin": "Pedido expresso da Tempo, 29/10/2026",
|
||||
"collapsed": "Mais filtros",
|
||||
"expanded": "Menos filtros",
|
||||
"verification": {
|
||||
"result": "passed",
|
||||
"checks": [
|
||||
"Mais filtros quando recolhido",
|
||||
"Menos filtros quando aberto",
|
||||
"Alternância por clique e teclado",
|
||||
"Escolhas e contagem conservadas"
|
||||
],
|
||||
"scope": "Componente React real, compilado e exercitado em Chromium; sem backend ou base de dados."
|
||||
},
|
||||
"deployment": "Alteração local; sem instalação."
|
||||
}
|
||||
}
|
||||
@@ -7620,6 +7620,7 @@ Cobertura: identificação/período, 27 campos manuais IVA e 24 campos manuais M
|
||||
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
|
||||
|
||||
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
@@ -9071,3 +9072,5 @@ Validação e diagnósticos:29 Feature/364 finais, com seis novos de reutilizaç
|
||||
Rastreabilidade dos executores: o recibo anterior organization_country_editor_increment_20261103 conservava na lista tooling_sha256 nomes de helpers da etapa workspace, por uma cópia de metadados. Mantêm-se esse recibo e todas as suas provas exactamente como estavam. Este incremento acrescenta prior_tooling_supplement_sha256 para os ficheiros efectivamente usados e retidos de check-organization-country-editor-qa, verify-organization-country-editor-concurrency e record-organization-country-editor, e liga os próprios executores actuais pelos caminhos correctos, incluindo este recorder. A suplementação de rastreabilidade não altera os resultados, fontes, observações de concorrência ou estado de instalação anteriores.
|
||||
|
||||
Preservação e próxima acção:79 tabelas de teste vazias, lock/identidade/owner/RLS/FORCE conferidos e fixture de falha revertida.89 recibos/411 provas anteriores preservados e16 novas provas arquivadas. Nenhuma migração/escrita activa ou comunicação externa. AUD-COUNTRY-02 e AUD-PROFILE-25 continuam parciais: clientes legados sem chave mantêm semântica anterior; restam outros consumidores/perfis, retoma durável/expiração, visual/móvel/browser, preenchimento real autorizado onde falta país, backup/restauro do candidato e disponibilização autenticada. A fonte aprovada não volta a ser submetida a uma validação genérica nem se inventam representantes. Próxima acção: confrontar os consumidores restantes e a recuperação de preparações com a matriz, reutilizando os mecanismos agora comuns.
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,284 @@
|
||||
AGENTS.md | 4 [32m+[m
|
||||
app/Exceptions/OpportunityWorkflowUnavailable.php | 21 [32m+[m
|
||||
.../Controllers/Api/V1/OpportunityController.php | 2 [32m+[m
|
||||
.../Controllers/CompanyComparisonController.php | 33 [32m+[m
|
||||
.../Controllers/CompanyPreparationController.php | 24 [32m+[m
|
||||
app/Http/Controllers/CrmController.php | 90 [32m+[m[31m-[m
|
||||
app/Http/Controllers/DashboardController.php | 13 [32m+[m[31m-[m
|
||||
.../DeskRequestCheckpointController.php | 20 [32m+[m
|
||||
.../Controllers/DeskServiceEditorController.php | 7 [32m+[m[31m-[m
|
||||
app/Http/Controllers/DocumentMalwareController.php | 30 [32m+[m[31m-[m
|
||||
app/Http/Controllers/EducationController.php | 147 [32m+[m[31m-[m
|
||||
.../EducationDeclarationDraftController.php | 34 [32m+[m
|
||||
.../Controllers/ExistingBusinessController.php | 2 [32m+[m
|
||||
.../Controllers/InvestmentDiscoveryController.php | 8 [32m+[m[31m-[m
|
||||
app/Http/Controllers/InvestmentGuideController.php | 31 [32m+[m
|
||||
.../Controllers/InvestmentPortfolioController.php | 8 [32m+[m[31m-[m
|
||||
.../Controllers/InvestmentWorkspaceController.php | 49 [32m+[m[31m-[m
|
||||
app/Http/Controllers/InvestmentZoneController.php | 4 [32m+[m
|
||||
app/Http/Controllers/InvestorPathController.php | 16 [32m+[m[31m-[m
|
||||
app/Http/Controllers/LegacyContentController.php | 2 [32m+[m
|
||||
.../OpportunityManagementController.php | 2 [32m+[m
|
||||
.../Controllers/OpportunityResourceController.php | 30 [32m+[m[31m-[m
|
||||
.../Controllers/PartnershipExchangeController.php | 206 [32m+[m[31m-[m
|
||||
app/Http/Controllers/PortalProfileController.php | 15 [32m+[m[31m-[m
|
||||
.../PortalProfileInstallationController.php | 14 [32m+[m
|
||||
app/Http/Controllers/PortalServicesController.php | 3 [32m+[m
|
||||
app/Http/Controllers/ProactiveSupplyController.php | 15 [32m+[m
|
||||
.../Controllers/ProductExperimentController.php | 41 [32m+[m
|
||||
.../Controllers/ProductUsageMetricsController.php | 19 [32m+[m
|
||||
.../PublicBusinessOpportunityController.php | 43 [32m+[m[31m-[m
|
||||
.../Controllers/TechnicalSupportController.php | 45 [32m+[m
|
||||
app/Http/Controllers/VirtualDeskController.php | 173 [32m+[m[31m-[m
|
||||
.../VirtualDeskInstallationController.php | 11 [32m+[m[31m-[m
|
||||
.../VirtualDeskOperationsController.php | 86 [32m+[m[31m-[m
|
||||
app/Http/Middleware/CollectPublicSearchMetrics.php | 25 [32m+[m
|
||||
.../PublicBusinessOpportunityResource.php | 5 [32m+[m
|
||||
app/Jobs/ScanDocumentForMalware.php | 127 [32m+[m[31m-[m
|
||||
app/Models/CrmTask.php | 21 [32m+[m
|
||||
app/Services/Ai/AssistiveRules.php | 5 [32m+[m[31m-[m
|
||||
app/Services/Ai/InvestorGuidance.php | 12 [32m+[m[31m-[m
|
||||
app/Services/BusinessFormSchema.php | 41 [32m+[m[31m-[m
|
||||
app/Services/BusinessPointPublication.php | 125 [32m+[m
|
||||
app/Services/BusinessProximitySearch.php | 53 [32m+[m
|
||||
app/Services/CompanyComparison.php | 208 [32m+[m
|
||||
app/Services/CompanyPreparation.php | 285 [32m+[m
|
||||
app/Services/CrmNextAction.php | 27 [32m+[m
|
||||
app/Services/CrmTaskCreation.php | 76 [32m+[m
|
||||
app/Services/CrmTaskPreparations.php | 117 [32m+[m
|
||||
app/Services/DashboardWorkQueue.php | 88 [32m+[m
|
||||
app/Services/DeskAttachments.php | 116 [32m+[m
|
||||
app/Services/DeskPaymentEvidence.php | 46 [32m+[m
|
||||
app/Services/DeskRequestCheckpoints.php | 88 [32m+[m
|
||||
app/Services/DeskServiceAvailability.php | 47 [32m+[m
|
||||
app/Services/DeskServiceEditor.php | 103 [32m+[m[31m-[m
|
||||
app/Services/EducationAccess.php | 21 [32m+[m
|
||||
app/Services/EducationDeclarationDrafts.php | 142 [32m+[m
|
||||
app/Services/EducationDeclarationRules.php | 29 [32m+[m
|
||||
app/Services/EducationReviews.php | 101 [32m+[m
|
||||
app/Services/EducationWorkQueue.php | 40 [32m+[m
|
||||
app/Services/ExistingBusinessDrafts.php | 3 [32m+[m[31m-[m
|
||||
app/Services/ExistingBusinessIntake.php | 4 [32m+[m[31m-[m
|
||||
app/Services/InvestmentGuides.php | 169 [32m+[m
|
||||
app/Services/InvestmentProfileCommands.php | 68 [32m+[m
|
||||
app/Services/InvestorPath.php | 4 [32m+[m[31m-[m
|
||||
app/Services/Malware/DocumentScanAttempts.php | 76 [32m+[m
|
||||
app/Services/Malware/DocumentScanDelivery.php | 46 [32m+[m
|
||||
app/Services/Malware/DocumentScanRetry.php | 78 [32m+[m
|
||||
app/Services/OpportunityImageProcessor.php | 18 [32m+[m[31m-[m
|
||||
app/Services/OpportunityProfileWorkflow.php | 12 [32m+[m
|
||||
app/Services/OpportunityResourceFileCleanup.php | 65 [32m+[m
|
||||
app/Services/OpportunityResources.php | 24 [32m+[m[31m-[m
|
||||
app/Services/OpportunityScanDispatch.php | 50 [32m+[m
|
||||
app/Services/OpportunityTechnicalExecution.php | 40 [32m+[m
|
||||
app/Services/OpportunityTechnicalIncidents.php | 134 [32m+[m
|
||||
app/Services/OpportunityTechnicalSupport.php | 128 [32m+[m
|
||||
app/Services/OpportunityWorkflowExecution.php | 28 [32m+[m
|
||||
app/Services/OrganizationCountry.php | 28 [32m+[m[31m-[m
|
||||
app/Services/PartnershipAgreementHistory.php | 141 [32m+[m
|
||||
app/Services/PartnershipAgreementProjection.php | 84 [32m+[m
|
||||
app/Services/PartnershipAgreements.php | 140 [32m+[m
|
||||
app/Services/PartnershipCandidacies.php | 188 [32m+[m
|
||||
app/Services/PartnershipCandidacyHistory.php | 198 [32m+[m
|
||||
app/Services/PartnershipDraftSaves.php | 46 [32m+[m
|
||||
app/Services/PartnershipExchange.php | 250 [32m+[m[31m-[m
|
||||
app/Services/PartnershipMilestones.php | 78 [32m+[m
|
||||
app/Services/PartnershipNotifications.php | 145 [32m+[m
|
||||
app/Services/PartnershipPublication.php | 110 [32m+[m
|
||||
app/Services/PartnershipPublicationHistory.php | 195 [32m+[m
|
||||
app/Services/PartnershipRoomUploads.php | 69 [32m+[m
|
||||
app/Services/PortalAudit.php | 15 [32m+[m[31m-[m
|
||||
app/Services/PortalProfileAccess.php | 4 [32m+[m
|
||||
app/Services/PortalProfileAdministration.php | 6 [32m+[m[31m-[m
|
||||
app/Services/PortalProfileInstallation.php | 107 [32m+[m[31m-[m
|
||||
app/Services/ProactiveSupply.php | 79 [32m+[m[31m-[m
|
||||
app/Services/ProductExperiments.php | 307 [32m+[m
|
||||
app/Services/ProductUsageMetrics.php | 170 [32m+[m
|
||||
app/Services/PublicBusinessLocations.php | 57 [32m+[m
|
||||
app/Services/PublicSearchMetrics.php | 144 [32m+[m
|
||||
app/Services/TechnicalIncidentHandling.php | 138 [32m+[m
|
||||
app/Services/TechnicalSupportProfileSchema.php | 40 [32m+[m
|
||||
app/Services/VerifiedDocumentBinary.php | 2 [32m+[m[31m-[m
|
||||
app/Services/VirtualDesk.php | 177 [32m+[m[31m-[m
|
||||
app/Services/VirtualDeskInstallation.php | 3 [32m+[m
|
||||
bootstrap/app.php | 2 [32m+[m
|
||||
config/bvm-admission-guide.php | 101 [32m+[m
|
||||
config/company-comparison.php | 30 [32m+[m
|
||||
config/investment-guides.php | 37 [32m+[m
|
||||
config/product-experiments.php | 17 [32m+[m
|
||||
config/product-metrics.php | 26 [32m+[m
|
||||
config/search-metrics.php | 30 [32m+[m
|
||||
...6_11_02_230000_education_declaration_drafts.php | 38 [32m+[m
|
||||
...3_000000_existing_business_location_sources.php | 27 [32m+[m
|
||||
...6_11_03_010000_desk_service_effective_dates.php | 24 [32m+[m
|
||||
...026_11_03_020000_create_product_experiments.php | 71 [32m+[m
|
||||
...6_11_03_030000_create_product_search_counts.php | 50 [32m+[m
|
||||
...1_03_040000_create_desk_request_checkpoints.php | 33 [32m+[m
|
||||
...2026_11_03_050000_scope_desk_document_links.php | 26 [32m+[m
|
||||
..._03_060000_crm_task_creation_commands_index.php | 17 [32m+[m
|
||||
...6_11_03_070000_create_crm_task_preparations.php | 32 [32m+[m
|
||||
..._11_03_080000_add_technical_support_profile.php | 26 [32m+[m
|
||||
.../unification/local-validation-20261029.json | 51257 [32m+++++++++++++++++++[m
|
||||
.../unification/traceability-and-manual.md | 1058 [32m+[m
|
||||
docs/investmoz/v2/auditoria-portal/backlog.md | 247 [32m+[m[31m-[m
|
||||
.../v2/auditoria-portal/matriz-requisitos.csv | 393 [32m+[m[31m-[m
|
||||
.../auditoria-portal/visao-consolidada-modulos.md | 184 [32m+[m[31m-[m
|
||||
resources/js/components/business-point-summary.tsx | 131 [32m+[m
|
||||
resources/js/components/crm-edit-comparison.tsx | 120 [32m+[m
|
||||
resources/js/components/crm-task-creation.tsx | 293 [32m+[m
|
||||
resources/js/components/dashboard-work-queue.tsx | 167 [32m+[m
|
||||
resources/js/components/desk-attachment-field.tsx | 453 [32m+[m
|
||||
resources/js/components/desk-case-state-form.tsx | 124 [32m+[m
|
||||
resources/js/components/desk-document-picker.tsx | 175 [32m+[m
|
||||
resources/js/components/desk-draft-recovery.tsx | 691 [32m+[m
|
||||
.../js/components/desk-internal-operations.tsx | 59 [32m+[m[31m-[m
|
||||
.../js/components/desk-payment-document-form.tsx | 138 [32m+[m
|
||||
resources/js/components/desk-service-history.tsx | 12 [32m+[m
|
||||
resources/js/components/desk-typed-choice.tsx | 88 [32m+[m
|
||||
resources/js/components/education-review.tsx | 229 [32m+[m
|
||||
.../js/components/education-submitted-snapshot.tsx | 110 [32m+[m
|
||||
.../js/components/funding-criteria-assessment.tsx | 197 [32m+[m
|
||||
.../js/components/investment-profile-editor.tsx | 313 [32m+[m
|
||||
resources/js/components/investor-guidance.tsx | 44 [32m+[m[31m-[m
|
||||
.../components/opportunity-technical-incidents.tsx | 81 [32m+[m
|
||||
.../js/components/organization-country-editor.tsx | 233 [32m+[m
|
||||
.../js/components/organization-country-reuse.tsx | 155 [32m+[m
|
||||
.../partnership-agreement-instrument.tsx | 86 [32m+[m
|
||||
.../js/components/partnership-candidacies.tsx | 730 [32m+[m
|
||||
.../js/components/partnership-edit-comparison.tsx | 275 [32m+[m
|
||||
.../components/partnership-publication-summary.tsx | 85 [32m+[m
|
||||
.../js/components/partnership-publication.tsx | 210 [32m+[m
|
||||
.../js/components/partnership-room-comparison.tsx | 175 [32m+[m
|
||||
resources/js/components/proactive-supply.tsx | 31 [32m+[m[31m-[m
|
||||
resources/js/components/profile-navigation.tsx | 13 [32m+[m
|
||||
resources/js/components/search-usage-report.tsx | 118 [32m+[m
|
||||
resources/js/components/similar-businesses.tsx | 250 [32m+[m[31m-[m
|
||||
.../js/components/technical-incident-handling.tsx | 122 [32m+[m
|
||||
.../js/components/technical-incident-notice.tsx | 108 [32m+[m
|
||||
resources/js/lib/business-locations.ts | 52 [32m+[m
|
||||
resources/js/lib/company-comparison.ts | 17 [32m+[m
|
||||
resources/js/lib/company-preparation.ts | 72 [32m+[m
|
||||
resources/js/lib/crm-record-edit.ts | 213 [32m+[m
|
||||
resources/js/lib/crm-task-creation.ts | 371 [32m+[m
|
||||
resources/js/lib/crm-task-preparation.ts | 384 [32m+[m
|
||||
resources/js/lib/desk-checkpoints.ts | 154 [32m+[m
|
||||
resources/js/lib/desk-field-values.ts | 61 [32m+[m
|
||||
resources/js/lib/desk-field-visibility.ts | 19 [32m+[m[31m-[m
|
||||
resources/js/lib/education-review.ts | 388 [32m+[m
|
||||
resources/js/lib/investment-guides.ts | 34 [32m+[m
|
||||
resources/js/lib/investment-profile-editor.ts | 433 [32m+[m
|
||||
resources/js/lib/organization-country-reuse.ts | 325 [32m+[m
|
||||
resources/js/lib/page-preparation-guard.ts | 28 [32m+[m
|
||||
resources/js/lib/partnership-agreement.ts | 63 [32m+[m
|
||||
resources/js/lib/partnership-candidacy-edit.ts | 805 [32m+[m
|
||||
resources/js/lib/partnership-edit.ts | 862 [32m+[m
|
||||
resources/js/lib/partnership-history.ts | 26 [32m+[m
|
||||
resources/js/lib/partnership-publication.ts | 465 [32m+[m
|
||||
resources/js/lib/partnership-room-delivery.ts | 199 [32m+[m
|
||||
resources/js/lib/partnership-room-edit.ts | 845 [32m+[m
|
||||
resources/js/lib/product-experiments.ts | 107 [32m+[m
|
||||
resources/js/lib/product-usage.ts | 69 [32m+[m
|
||||
resources/js/lib/profile-json.ts | 14 [32m+[m[31m-[m
|
||||
resources/js/lib/profile-support-installation.ts | 406 [32m+[m
|
||||
resources/js/lib/technical-incident-handling.ts | 368 [32m+[m
|
||||
resources/js/lib/technical-incident-labels.ts | 45 [32m+[m
|
||||
resources/js/pages/crm/investor.tsx | 116 [32m+[m[31m-[m
|
||||
resources/js/pages/crm/lead.tsx | 207 [32m+[m[31m-[m
|
||||
resources/js/pages/crm/types.ts | 3 [32m+[m
|
||||
resources/js/pages/dashboard.tsx | 12 [32m+[m
|
||||
resources/js/pages/desk/case.tsx | 240 [32m+[m[31m-[m
|
||||
resources/js/pages/desk/catalogue.tsx | 20 [32m+[m[31m-[m
|
||||
resources/js/pages/desk/request.tsx | 723 [32m+[m[31m-[m
|
||||
resources/js/pages/desk/service-editor.tsx | 337 [32m+[m[31m-[m
|
||||
resources/js/pages/education/declaration.tsx | 154 [32m+[m
|
||||
resources/js/pages/education/index.tsx | 728 [32m+[m[31m-[m
|
||||
resources/js/pages/funding/show.tsx | 157 [32m+[m[31m-[m
|
||||
.../js/pages/investment/business-marketplace.tsx | 82 [32m+[m[31m-[m
|
||||
.../js/pages/investment/business-opportunity.tsx | 25 [32m+[m[31m-[m
|
||||
resources/js/pages/investment/business-wizard.tsx | 88 [32m+[m[31m-[m
|
||||
resources/js/pages/investment/business.tsx | 31 [32m+[m[31m-[m
|
||||
.../js/pages/investment/company-comparison.tsx | 386 [32m+[m
|
||||
.../js/pages/investment/company-preparation.tsx | 790 [32m+[m
|
||||
resources/js/pages/investment/guide.tsx | 676 [32m+[m
|
||||
resources/js/pages/investment/profile.tsx | 293 [32m+[m[31m-[m
|
||||
resources/js/pages/investment/zone-attachments.tsx | 18 [32m+[m[31m-[m
|
||||
resources/js/pages/investor-path/index.tsx | 41 [32m+[m[31m-[m
|
||||
resources/js/pages/investor-path/show.tsx | 23 [32m+[m
|
||||
resources/js/pages/management/experiments/edit.tsx | 722 [32m+[m
|
||||
.../js/pages/management/experiments/index.tsx | 122 [32m+[m
|
||||
.../js/pages/management/opportunity-draft.tsx | 8 [32m+[m
|
||||
.../js/pages/management/opportunity-resources.tsx | 6 [32m+[m
|
||||
.../pages/management/proactive-supply-review.tsx | 419 [32m+[m
|
||||
resources/js/pages/management/product-usage.tsx | 267 [32m+[m
|
||||
resources/js/pages/management/shared.tsx | 10 [32m+[m[31m-[m
|
||||
.../js/pages/partnerships/agreement-history.tsx | 199 [32m+[m
|
||||
resources/js/pages/partnerships/dashboard.tsx | 51 [32m+[m[31m-[m
|
||||
resources/js/pages/partnerships/detail.tsx | 508 [32m+[m[31m-[m
|
||||
resources/js/pages/partnerships/editor.tsx | 824 [32m+[m[31m-[m
|
||||
.../js/pages/partnerships/interest-history.tsx | 350 [32m+[m
|
||||
.../js/pages/partnerships/publication-history.tsx | 349 [32m+[m
|
||||
resources/js/pages/partnerships/room.tsx | 777 [32m+[m[31m-[m
|
||||
resources/js/pages/partnerships/shared.tsx | 6 [32m+[m
|
||||
resources/js/pages/portal/privacy-policy.tsx | 22 [32m+[m
|
||||
resources/js/pages/portal/services.tsx | 24 [32m+[m[31m-[m
|
||||
resources/js/pages/profiles/administration.tsx | 33 [32m+[m[31m-[m
|
||||
resources/js/pages/profiles/installation.tsx | 28 [32m+[m
|
||||
.../js/pages/profiles/support-installation.tsx | 170 [32m+[m
|
||||
resources/js/pages/profiles/technical-incident.tsx | 98 [32m+[m
|
||||
resources/js/pages/profiles/technical-support.tsx | 150 [32m+[m
|
||||
resources/js/types/profile-access.ts | 2 [32m+[m
|
||||
resources/views/desk/receipt-answers.blade.php | 9 [32m+[m
|
||||
resources/views/desk/receipt.blade.php | 16 [32m+[m
|
||||
routes/investor-path.php | 11 [32m+[m
|
||||
routes/partnerships.php | 3 [32m+[m
|
||||
routes/virtual-desk.php | 6 [32m+[m
|
||||
routes/web.php | 26 [32m+[m
|
||||
tests/Feature/BusinessPointPublicationTest.php | 242 [32m+[m
|
||||
tests/Feature/BusinessProximitySearchTest.php | 157 [32m+[m
|
||||
tests/Feature/BvmAdmissionGuideTest.php | 174 [32m+[m
|
||||
tests/Feature/CompanyComparisonTest.php | 199 [32m+[m
|
||||
tests/Feature/CompanyPreparationTest.php | 198 [32m+[m
|
||||
tests/Feature/CrmIntakeTest.php | 12 [32m+[m[31m-[m
|
||||
tests/Feature/CrmNextActionTest.php | 199 [32m+[m
|
||||
tests/Feature/CrmRecordVersionsTest.php | 140 [32m+[m
|
||||
tests/Feature/CrmTaskCreationTest.php | 168 [32m+[m
|
||||
tests/Feature/CrmTaskPreparationTest.php | 232 [32m+[m
|
||||
tests/Feature/DashboardWorkQueueTest.php | 275 [32m+[m
|
||||
tests/Feature/DeskAttachmentFieldsTest.php | 409 [32m+[m
|
||||
tests/Feature/DeskCaseStateTest.php | 132 [32m+[m
|
||||
tests/Feature/DeskClosedLifecycleTest.php | 164 [32m+[m
|
||||
tests/Feature/DeskDocumentReuseTest.php | 166 [32m+[m
|
||||
tests/Feature/DeskPaymentEvidenceTest.php | 213 [32m+[m
|
||||
tests/Feature/DeskRequestCheckpointTest.php | 204 [32m+[m
|
||||
tests/Feature/DeskServiceEffectiveDatesTest.php | 198 [32m+[m
|
||||
tests/Feature/DeskTypedFieldsTest.php | 247 [32m+[m
|
||||
tests/Feature/DocumentScanRetryTest.php | 204 [32m+[m
|
||||
tests/Feature/EducationDeclarationDraftsTest.php | 136 [32m+[m
|
||||
tests/Feature/EducationWorkQueueTest.php | 294 [32m+[m
|
||||
tests/Feature/InvestmentGuidesTest.php | 186 [32m+[m
|
||||
tests/Feature/InvestmentProfileCommandsTest.php | 107 [32m+[m
|
||||
tests/Feature/OpportunityCanonicalWorkflowTest.php | 120 [32m+[m
|
||||
.../Feature/OpportunityTechnicalIncidentsTest.php | 258 [32m+[m
|
||||
tests/Feature/OrganizationCountryCommandsTest.php | 130 [32m+[m
|
||||
.../OrganizationCountryReuseCommandsTest.php | 148 [32m+[m
|
||||
tests/Feature/PartnershipAgreementHistoryTest.php | 256 [32m+[m
|
||||
.../Feature/PartnershipAgreementProjectionTest.php | 235 [32m+[m
|
||||
tests/Feature/PartnershipAgreementRecoveryTest.php | 232 [32m+[m
|
||||
tests/Feature/PartnershipCandidaciesTest.php | 250 [32m+[m
|
||||
tests/Feature/PartnershipCandidacyHistoryTest.php | 204 [32m+[m
|
||||
tests/Feature/PartnershipDashboardPrivacyTest.php | 324 [32m+[m
|
||||
tests/Feature/PartnershipDraftRecoveryTest.php | 184 [32m+[m
|
||||
tests/Feature/PartnershipEditingTest.php | 211 [32m+[m
|
||||
tests/Feature/PartnershipParticipantsTest.php | 186 [32m+[m
|
||||
.../Feature/PartnershipPublicationHistoryTest.php | 223 [32m+[m
|
||||
.../Feature/PartnershipPublicationRecoveryTest.php | 189 [32m+[m
|
||||
.../Feature/PartnershipPublicationSummaryTest.php | 171 [32m+[m
|
||||
tests/Feature/PartnershipRoomRecoveryTest.php | 214 [32m+[m
|
||||
tests/Feature/ProactiveSupplyReviewTest.php | 188 [32m+[m
|
||||
tests/Feature/ProductExperimentsTest.php | 272 [32m+[m
|
||||
tests/Feature/ProductUsageMetricsTest.php | 206 [32m+[m
|
||||
tests/Feature/PublicSearchMetricsTest.php | 275 [32m+[m
|
||||
tests/Feature/ResourceTechnicalIncidentsTest.php | 247 [32m+[m
|
||||
tests/Feature/ScanTechnicalIncidentsTest.php | 422 [32m+[m
|
||||
tests/Feature/SupportIncidentNoticesTest.php | 173 [32m+[m
|
||||
tests/Feature/SupportProfileInstallationTest.php | 25
|
||||
@@ -0,0 +1,445 @@
|
||||
/* Presentation only: catalogue queries, records and permissions retain their contracts. */
|
||||
.investment-catalogue {
|
||||
--catalogue-accent: #c9141b;
|
||||
--catalogue-ink: #17324d;
|
||||
max-width: 1440px;
|
||||
color: var(--catalogue-ink);
|
||||
}
|
||||
.investment-catalogue > h1 {
|
||||
font-size: clamp(1.6rem, 3vw, 2.15rem);
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
.investment-catalogue .catalogue-intro {
|
||||
margin-top: 0.65rem;
|
||||
max-width: 76ch;
|
||||
color: #526477;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.catalogue-workspace {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
.catalogue-workspace > *,
|
||||
.catalogue-results > *,
|
||||
.catalogue-opportunity > * {
|
||||
min-width: 0;
|
||||
}
|
||||
.catalogue-filters {
|
||||
border: 1px solid #dce3eb;
|
||||
border-radius: 1rem;
|
||||
background: #fff;
|
||||
overflow: clip;
|
||||
}
|
||||
.catalogue-filters-toggle,
|
||||
.catalogue-filters-heading {
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 1rem 1.15rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
.catalogue-filters-toggle {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
min-height: 52px;
|
||||
}
|
||||
.catalogue-filters-toggle > svg:last-child {
|
||||
margin-left: auto;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.catalogue-filters-toggle[aria-expanded='true'] > svg:last-child {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.catalogue-filters-heading {
|
||||
display: none;
|
||||
}
|
||||
#catalogue-filter-fields {
|
||||
padding: 0 1.15rem 1.15rem;
|
||||
}
|
||||
.catalogue-filters input:not([type='checkbox']),
|
||||
.catalogue-filters select {
|
||||
min-height: 44px;
|
||||
max-width: 100%;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.catalogue-filter-group {
|
||||
border-top: 1px solid #e5eaf0;
|
||||
}
|
||||
.catalogue-filter-group > summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
min-height: 52px;
|
||||
padding: 0.65rem 0;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
.catalogue-filter-group > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.catalogue-filter-group > summary > svg {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.catalogue-filter-group[open] > summary > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.catalogue-filter-group-content {
|
||||
padding: 0.4rem 0 1rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
.catalogue-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.5rem;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 99px;
|
||||
background: #f0f3f7;
|
||||
color: #526477;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.catalogue-filters .catalogue-filter-group {
|
||||
margin-top: 0;
|
||||
}
|
||||
.catalogue-active-filters:empty {
|
||||
display: none;
|
||||
}
|
||||
.catalogue-active-filters button {
|
||||
border-radius: 0.5rem;
|
||||
background: #f5f7fa;
|
||||
border-color: #dce3eb;
|
||||
font-size: 0.75rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.catalogue-filter-submit > button:first-of-type {
|
||||
gap: 0.5rem;
|
||||
background: var(--catalogue-accent);
|
||||
min-height: 44px;
|
||||
}
|
||||
.catalogue-filter-submit > button:last-child {
|
||||
min-height: 44px;
|
||||
color: #526477;
|
||||
font-size: 0.875rem;
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.catalogue-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
.catalogue-results-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 1rem;
|
||||
}
|
||||
.catalogue-results-heading > h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.catalogue-results-heading > [role] {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #526477;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.catalogue-results-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem 1.25rem;
|
||||
padding-bottom: 1.1rem;
|
||||
border-bottom: 1px solid #dce3eb;
|
||||
}
|
||||
.catalogue-view-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid #dce3eb;
|
||||
border-radius: 0.75rem;
|
||||
background: #fff;
|
||||
}
|
||||
.catalogue-view-button {
|
||||
display: inline-flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 42px;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 0.5rem;
|
||||
color: #526477;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.catalogue-view-button[aria-pressed='true'] {
|
||||
background: #17324d;
|
||||
color: #fff;
|
||||
}
|
||||
.catalogue-view-button:not(:disabled):hover {
|
||||
color: var(--catalogue-accent);
|
||||
}
|
||||
.catalogue-view-button[aria-pressed='true']:not(:disabled):hover {
|
||||
background: var(--catalogue-accent);
|
||||
color: #fff;
|
||||
}
|
||||
.catalogue-view-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
.catalogue-utility-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 1.25rem;
|
||||
}
|
||||
.catalogue-utility-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 44px;
|
||||
font-size: 0.8rem;
|
||||
color: #526477;
|
||||
}
|
||||
.catalogue-utility-link[aria-disabled='true'] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.catalogue-cards {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
.catalogue-opportunity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #dce3eb;
|
||||
border-radius: 1rem;
|
||||
background: white;
|
||||
overflow: clip;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
.catalogue-opportunity:hover,
|
||||
.catalogue-opportunity:focus-within {
|
||||
border-color: #9cabbc;
|
||||
box-shadow: 0 5px 18px #17324d0a;
|
||||
}
|
||||
.catalogue-opportunity-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 0.85rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.catalogue-sector {
|
||||
padding: 0.25rem 0.6rem;
|
||||
background: #f0f4f8;
|
||||
color: #3d5268;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.catalogue-location {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
color: #526477;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.catalogue-location svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.catalogue-opportunity h2 {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.45;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--catalogue-ink);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.catalogue-summary {
|
||||
color: #526477;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.catalogue-opportunity-facts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.catalogue-maturity {
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 99px;
|
||||
background: #f5f6f8;
|
||||
color: #526477;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.catalogue-opportunity-actions {
|
||||
padding: 0.45rem 1.25rem 0.75rem;
|
||||
border-top: 1px solid #e5eaf0;
|
||||
}
|
||||
.catalogue-opportunity-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.75rem;
|
||||
}
|
||||
.catalogue-detail-link,
|
||||
.catalogue-favourite {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 44px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.catalogue-detail-link {
|
||||
color: var(--catalogue-accent);
|
||||
font-weight: 650;
|
||||
}
|
||||
.catalogue-favourite {
|
||||
color: #526477;
|
||||
text-align: left;
|
||||
}
|
||||
.catalogue-favourite[aria-pressed='true'] {
|
||||
color: var(--catalogue-accent);
|
||||
}
|
||||
.catalogue-opportunity-choices {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid #eef1f5;
|
||||
}
|
||||
.catalogue-opportunity-choices label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
min-height: 44px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
.catalogue-opportunity-choices input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--catalogue-accent);
|
||||
}
|
||||
.catalogue-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.catalogue-pagination a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #dce3eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #fff;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.catalogue-territorial {
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #dce3eb;
|
||||
}
|
||||
.catalogue-territorial > h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.catalogue-territorial > p {
|
||||
margin: 0.5rem 0 1.2rem;
|
||||
color: #526477;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.catalogue-territorial-links {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.catalogue-territorial-link {
|
||||
border: 1px solid #e5eaf0;
|
||||
border-radius: 0.65rem;
|
||||
padding: 0.85rem 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
.catalogue-territorial-link > span {
|
||||
color: #526477;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.catalogue-territorial-link h3 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* Headings and emphasized catalogue content use Tempo's red. */
|
||||
.investment-catalogue :is(h1, h2, h3, h4, h5, h6, caption, th, strong),
|
||||
.investment-catalogue :is(p.font-bold, p.font-semibold),
|
||||
.investment-catalogue .catalogue-sector,
|
||||
.investment-catalogue .catalogue-filters-toggle,
|
||||
.investment-catalogue .catalogue-territorial .catalogue-filter-group > summary {
|
||||
color: var(--catalogue-accent);
|
||||
}
|
||||
.investment-catalogue :is(a, button, summary, input, select):focus-visible {
|
||||
outline: 2px solid #17324d;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.investment-catalogue :is(a, button, summary):hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.catalogue-cards-grid,
|
||||
.catalogue-territorial-links {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.catalogue-workspace {
|
||||
grid-template-columns: 290px minmax(0, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
.catalogue-filters-toggle {
|
||||
display: none;
|
||||
}
|
||||
.catalogue-filters-heading {
|
||||
display: flex;
|
||||
}
|
||||
.catalogue-results-heading {
|
||||
min-height: 48px;
|
||||
}
|
||||
.catalogue-cards-list .catalogue-opportunity {
|
||||
flex-direction: row;
|
||||
}
|
||||
.catalogue-cards-list .catalogue-opportunity-actions {
|
||||
flex: 0 0 235px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-top: 0;
|
||||
border-left: 1px solid #e5eaf0;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.investment-catalogue * {
|
||||
transition: none;
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
|
||||
export default function CatalogueFilterGroup({
|
||||
title,
|
||||
expandedTitle,
|
||||
count = 0,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
expandedTitle?: string;
|
||||
count?: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<details
|
||||
className="catalogue-filter-group"
|
||||
onToggle={(event) => setOpen(event.currentTarget.open)}
|
||||
>
|
||||
<summary>
|
||||
<span>{open && expandedTitle ? expandedTitle : title}</span>
|
||||
{count > 0 && <span className="catalogue-count">{count}</span>}
|
||||
<ChevronDown size={16} aria-hidden="true" />
|
||||
</summary>
|
||||
<div className="catalogue-filter-group-content">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Field, input, button, groupLabels } from '@/pages/investment/shared';
|
||||
import type { References } from '@/pages/investment/shared';
|
||||
import { TerritoryLevel, type TerritoryOption } from './investment-territory';
|
||||
import CatalogueFilterGroup from './catalogue-filter-group';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ChevronDown, Search, SlidersHorizontal } from 'lucide-react';
|
||||
|
||||
export const modalityLabels: Record<string, string> = {
|
||||
investment_project: 'Projecto de investimento',
|
||||
@@ -83,6 +86,10 @@ export default function OpportunitySearchFilters({
|
||||
clear: () => void;
|
||||
errors: Record<string, string>;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
useEffect(() => {
|
||||
if (Object.keys(errors).length) setExpanded(true);
|
||||
}, [errors]);
|
||||
const select = (
|
||||
key: string,
|
||||
choices: { value: string; label: string }[],
|
||||
@@ -217,13 +224,14 @@ export default function OpportunitySearchFilters({
|
||||
);
|
||||
return (
|
||||
<form
|
||||
className="space-y-5 rounded-xl border bg-white p-4 sm:p-6"
|
||||
className="catalogue-filters"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
aria-label="Pesquisar oportunidades"
|
||||
>
|
||||
<<<<<<< HEAD
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Field label="O que procura">
|
||||
<input
|
||||
@@ -338,209 +346,394 @@ export default function OpportunitySearchFilters({
|
||||
label: c,
|
||||
})),
|
||||
'Seleccione uma moeda para comparar montantes. Não se efectuam conversões cambiais.',
|
||||
=======
|
||||
<button
|
||||
type="button"
|
||||
className="catalogue-filters-toggle"
|
||||
aria-expanded={expanded}
|
||||
aria-controls="catalogue-filter-fields"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
>
|
||||
<SlidersHorizontal size={18} aria-hidden="true" />
|
||||
<span>Filtros</span>
|
||||
{active.length + path.length > 0 && (
|
||||
<span className="catalogue-count">
|
||||
{active.length + path.length}
|
||||
</span>
|
||||
>>>>>>> origin/main
|
||||
)}
|
||||
</div>
|
||||
<details>
|
||||
<summary className="cursor-pointer rounded border p-3 font-semibold focus-visible:outline-2">
|
||||
Mais filtros
|
||||
</summary>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[
|
||||
'corridor_id',
|
||||
'cluster_id',
|
||||
'zee_id',
|
||||
'zfi_id',
|
||||
'park_id',
|
||||
'project_id',
|
||||
'subsector_id',
|
||||
'chain_id',
|
||||
].map((key) => select(key, choicesFor(key)))}
|
||||
<Field label="Data-alvo até">
|
||||
<ChevronDown size={18} aria-hidden="true" />
|
||||
</button>
|
||||
<h2 className="catalogue-filters-heading">
|
||||
<SlidersHorizontal size={17} aria-hidden="true" />
|
||||
Filtros
|
||||
</h2>
|
||||
<div
|
||||
id="catalogue-filter-fields"
|
||||
className={`${expanded ? 'block' : 'hidden'} space-y-5 lg:block`}
|
||||
>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<Field label="O que procura">
|
||||
<input
|
||||
className={input}
|
||||
type="date"
|
||||
value={String(data.before)}
|
||||
onChange={(e) => change({ before: e.target.value })}
|
||||
value={String(data.q)}
|
||||
placeholder="Palavras do título ou da descrição"
|
||||
onChange={(e) => change({ q: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
{select('perspective', [
|
||||
{ value: 'exists', label: 'O que existe' },
|
||||
{ value: 'needs', label: 'Necessidades identificadas' },
|
||||
{ value: 'ideas', label: 'Ideias de investimento' },
|
||||
{
|
||||
value: 'opportunities',
|
||||
label: 'Oportunidades cadastradas — disponibilidade na ficha',
|
||||
},
|
||||
])}
|
||||
{select('sector', choicesFor('sector'))}
|
||||
{select('category', choicesFor('category'))}
|
||||
</div>
|
||||
<p className="my-3 text-sm">
|
||||
Estes filtros procuram associações documentadas. Estar perto
|
||||
de um parque ou no mesmo distrito não constitui associação,
|
||||
nem confirma incentivos ou disponibilidade.
|
||||
</p>
|
||||
<label className="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(data.include_national)}
|
||||
onChange={(e) =>
|
||||
change({ include_national: e.target.checked })
|
||||
}
|
||||
/>
|
||||
Incluir oportunidades de âmbito nacional
|
||||
</label>
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer font-medium">
|
||||
Outras características
|
||||
</summary>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<CatalogueFilterGroup title="Localização" count={path.length}>
|
||||
<p className="text-sm leading-relaxed text-slate-600">
|
||||
A província inclui as oportunidades dos seus distritos,
|
||||
postos administrativos e localidades. Não é necessário
|
||||
escolher todos os níveis. O contexto provincial de um
|
||||
corredor é identificado nos resultados; não confirma uma
|
||||
implantação distrital.
|
||||
{path.length > 1 && (
|
||||
<button type="button" className="ml-2 underline" onClick={() => territory(path.slice(0, 1))}>
|
||||
Ver toda a província de {path[0].name}
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
<fieldset className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<legend className="sr-only">Localização</legend>
|
||||
{(
|
||||
[
|
||||
'province_id',
|
||||
'district_id',
|
||||
'admin_post_id',
|
||||
'locality_id',
|
||||
] as const
|
||||
).map((key, index) => (
|
||||
<TerritoryLevel
|
||||
key={`${key}-${path[index - 1]?.id ?? ''}`}
|
||||
index={index}
|
||||
parent={path[index - 1]?.id ?? ''}
|
||||
selected={path[index]}
|
||||
endpoint="/api/v1/investment/territory-options"
|
||||
onChange={(value) =>
|
||||
territory([
|
||||
...path.slice(0, index),
|
||||
...(value ? [value] : []),
|
||||
])
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
</CatalogueFilterGroup>
|
||||
<CatalogueFilterGroup
|
||||
title="Preparação e investimento"
|
||||
count={
|
||||
[
|
||||
'preparation',
|
||||
'min_amount',
|
||||
'max_amount',
|
||||
'currency',
|
||||
].filter((key) => data[key]).length
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<Field label="Estado de preparação">
|
||||
<select
|
||||
className={input}
|
||||
value={String(data.preparation ?? '')}
|
||||
onChange={(e) =>
|
||||
change({
|
||||
preparation: e.target.value,
|
||||
maturity_level: '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Todos os estados</option>
|
||||
{[
|
||||
'Etapas de preparação',
|
||||
'Classificações históricas',
|
||||
].map((group) => (
|
||||
<optgroup key={group} label={group}>
|
||||
{(references.preparationOptions ?? [])
|
||||
.filter((o) => o.group === group)
|
||||
.map((o) => (
|
||||
<option
|
||||
key={o.value}
|
||||
value={o.value}
|
||||
>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-sm text-slate-600">
|
||||
Os registos históricos são apresentados
|
||||
separadamente e conservam a classificação
|
||||
indicada na ficha.
|
||||
</p>
|
||||
</Field>
|
||||
{(['min_amount', 'max_amount'] as const).map((key) => (
|
||||
<Field key={key} label={labels[key]}>
|
||||
<input
|
||||
className={input}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={String(data[key])}
|
||||
onChange={(e) =>
|
||||
change({ [key]: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
{select(
|
||||
'currency',
|
||||
['MZN', 'USD', 'EUR', 'ZAR'].map((c) => ({
|
||||
value: c,
|
||||
label: c,
|
||||
})),
|
||||
'Seleccione uma moeda para comparar montantes. Não se efectuam conversões cambiais.',
|
||||
)}
|
||||
</div>
|
||||
</CatalogueFilterGroup>
|
||||
<CatalogueFilterGroup
|
||||
title="Mais filtros"
|
||||
expandedTitle="Menos filtros"
|
||||
count={
|
||||
active.filter(([key]) =>
|
||||
[
|
||||
'corridor_id',
|
||||
'cluster_id',
|
||||
'zee_id',
|
||||
'zfi_id',
|
||||
'park_id',
|
||||
'project_id',
|
||||
'subsector_id',
|
||||
'chain_id',
|
||||
'before',
|
||||
'include_national',
|
||||
'facets',
|
||||
].includes(key),
|
||||
).length
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
|
||||
{[
|
||||
'education',
|
||||
'infrastructure',
|
||||
'labour',
|
||||
'qualification',
|
||||
'support',
|
||||
'impact',
|
||||
'partnership',
|
||||
]
|
||||
.filter((group) =>
|
||||
references.options.some(
|
||||
(o) => o.group === group,
|
||||
),
|
||||
)
|
||||
.map((group) => (
|
||||
<fieldset
|
||||
key={group}
|
||||
className="rounded border p-3"
|
||||
>
|
||||
<legend>{groupLabels[group]}</legend>
|
||||
{references.options
|
||||
.filter((o) => o.group === group)
|
||||
.map((option) => (
|
||||
<label
|
||||
key={option.id}
|
||||
className="flex gap-2 py-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
Array.isArray(
|
||||
data.facets,
|
||||
) &&
|
||||
data.facets.includes(
|
||||
option.id,
|
||||
)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const selected =
|
||||
'corridor_id',
|
||||
'cluster_id',
|
||||
'zee_id',
|
||||
'zfi_id',
|
||||
'park_id',
|
||||
'project_id',
|
||||
'subsector_id',
|
||||
'chain_id',
|
||||
].map((key) => select(key, choicesFor(key)))}
|
||||
<Field label="Data-alvo até">
|
||||
<input
|
||||
className={input}
|
||||
type="date"
|
||||
value={String(data.before)}
|
||||
onChange={(e) =>
|
||||
change({ before: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<p className="my-3 text-sm">
|
||||
Estes filtros procuram associações documentadas. Estar
|
||||
perto de um parque ou no mesmo distrito não constitui
|
||||
associação, nem confirma incentivos ou disponibilidade.
|
||||
</p>
|
||||
<label className="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(data.include_national)}
|
||||
onChange={(e) =>
|
||||
change({ include_national: e.target.checked })
|
||||
}
|
||||
/>
|
||||
Incluir oportunidades de âmbito nacional
|
||||
</label>
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer font-medium">
|
||||
Outras características
|
||||
</summary>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||||
{[
|
||||
'education',
|
||||
'infrastructure',
|
||||
'labour',
|
||||
'qualification',
|
||||
'support',
|
||||
'impact',
|
||||
'partnership',
|
||||
]
|
||||
.filter((group) =>
|
||||
references.options.some(
|
||||
(o) => o.group === group,
|
||||
),
|
||||
)
|
||||
.map((group) => (
|
||||
<fieldset
|
||||
key={group}
|
||||
className="rounded border p-3"
|
||||
>
|
||||
<legend>{groupLabels[group]}</legend>
|
||||
{references.options
|
||||
.filter((o) => o.group === group)
|
||||
.map((option) => (
|
||||
<label
|
||||
key={option.id}
|
||||
className="flex gap-2 py-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
Array.isArray(
|
||||
data.facets,
|
||||
) &&
|
||||
data.facets.includes(
|
||||
option.id,
|
||||
)
|
||||
? data.facets
|
||||
: [];
|
||||
change({
|
||||
facets: e.target
|
||||
.checked
|
||||
? [
|
||||
...selected,
|
||||
option.id,
|
||||
]
|
||||
: selected.filter(
|
||||
(id) =>
|
||||
id !==
|
||||
}
|
||||
onChange={(e) => {
|
||||
const selected =
|
||||
Array.isArray(
|
||||
data.facets,
|
||||
)
|
||||
? data.facets
|
||||
: [];
|
||||
change({
|
||||
facets: e.target
|
||||
.checked
|
||||
? [
|
||||
...selected,
|
||||
option.id,
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</details>
|
||||
<section
|
||||
aria-label="Filtros activos"
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{path.map((node, index) => (
|
||||
<button
|
||||
className="rounded border px-3 py-2"
|
||||
type="button"
|
||||
key={node.id}
|
||||
aria-label={`Remover localização ${node.name}`}
|
||||
onClick={() => territory(path.slice(0, index))}
|
||||
>
|
||||
{node.name} ×
|
||||
</button>
|
||||
))}
|
||||
{active.flatMap(([key, value]) =>
|
||||
key === 'facets' && Array.isArray(value)
|
||||
? value.map((id) => (
|
||||
<button
|
||||
key={`facet-${id}`}
|
||||
type="button"
|
||||
className="rounded border px-3 py-2"
|
||||
onClick={() =>
|
||||
change({
|
||||
facets: value.filter((x) => x !== id),
|
||||
})
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
{references.options.find((o) => o.id === id)
|
||||
?.label ?? 'Critério adicional'}{' '}
|
||||
×
|
||||
</button>
|
||||
))
|
||||
: [
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className="rounded border px-3 py-2 text-left"
|
||||
aria-label={`Remover ${labels[key] ?? 'critério adicional'}`}
|
||||
onClick={() => remove(key)}
|
||||
>
|
||||
{labels[key] ?? 'Critério adicional'}:{' '}
|
||||
{typeof value === 'boolean'
|
||||
? 'Sim'
|
||||
: (choicesFor(key).find(
|
||||
(o) => o.value === String(value),
|
||||
)?.label ??
|
||||
([
|
||||
'q',
|
||||
'min_amount',
|
||||
'max_amount',
|
||||
'currency',
|
||||
'before',
|
||||
'province',
|
||||
'district',
|
||||
].includes(key)
|
||||
? String(value)
|
||||
: 'seleccionado'))}{' '}
|
||||
×
|
||||
</button>,
|
||||
],
|
||||
)}
|
||||
</section>
|
||||
{Object.entries(errors).map(([key, value]) => (
|
||||
<p key={key} role="alert">
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
{select('sort', [
|
||||
{ value: 'recent', label: 'Mais recentes' },
|
||||
{ value: 'title', label: 'Título' },
|
||||
{
|
||||
value: 'amount_asc',
|
||||
label: 'Montante crescente, por moeda',
|
||||
},
|
||||
{
|
||||
value: 'amount_desc',
|
||||
label: 'Montante decrescente, por moeda',
|
||||
},
|
||||
])}
|
||||
<button className={button}>Pesquisar</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border px-4 py-2 underline"
|
||||
onClick={clear}
|
||||
]
|
||||
: selected.filter(
|
||||
(
|
||||
id,
|
||||
) =>
|
||||
id !==
|
||||
option.id,
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</CatalogueFilterGroup>
|
||||
<section
|
||||
aria-label="Filtros activos"
|
||||
className="catalogue-active-filters flex flex-wrap gap-2"
|
||||
>
|
||||
Limpar filtros
|
||||
</button>
|
||||
{path.map((node, index) => (
|
||||
<button
|
||||
className="rounded border px-3 py-2"
|
||||
type="button"
|
||||
key={node.id}
|
||||
aria-label={`Remover localização ${node.name}`}
|
||||
onClick={() => territory(path.slice(0, index))}
|
||||
>
|
||||
{node.name} ×
|
||||
</button>
|
||||
))}
|
||||
{active.flatMap(([key, value]) =>
|
||||
key === 'facets' && Array.isArray(value)
|
||||
? value.map((id) => (
|
||||
<button
|
||||
key={`facet-${id}`}
|
||||
type="button"
|
||||
className="rounded border px-3 py-2"
|
||||
onClick={() =>
|
||||
change({
|
||||
facets: value.filter(
|
||||
(x) => x !== id,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
{references.options.find(
|
||||
(o) => o.id === id,
|
||||
)?.label ?? 'Critério adicional'}{' '}
|
||||
×
|
||||
</button>
|
||||
))
|
||||
: [
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className="rounded border px-3 py-2 text-left"
|
||||
aria-label={`Remover ${labels[key] ?? 'critério adicional'}`}
|
||||
onClick={() => remove(key)}
|
||||
>
|
||||
{labels[key] ?? 'Critério adicional'}:{' '}
|
||||
{typeof value === 'boolean'
|
||||
? 'Sim'
|
||||
: (choicesFor(key).find(
|
||||
(o) =>
|
||||
o.value === String(value),
|
||||
)?.label ??
|
||||
([
|
||||
'q',
|
||||
'min_amount',
|
||||
'max_amount',
|
||||
'currency',
|
||||
'before',
|
||||
'province',
|
||||
'district',
|
||||
].includes(key)
|
||||
? String(value)
|
||||
: 'seleccionado'))}{' '}
|
||||
×
|
||||
</button>,
|
||||
],
|
||||
)}
|
||||
</section>
|
||||
{Object.entries(errors).map(([key, value]) => (
|
||||
<p key={key} role="alert">
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
<div className="catalogue-filter-submit grid gap-3">
|
||||
{select('sort', [
|
||||
{ value: 'recent', label: 'Mais recentes' },
|
||||
{ value: 'title', label: 'Título' },
|
||||
{
|
||||
value: 'amount_asc',
|
||||
label: 'Montante crescente, por moeda',
|
||||
},
|
||||
{
|
||||
value: 'amount_desc',
|
||||
label: 'Montante decrescente, por moeda',
|
||||
},
|
||||
])}
|
||||
<button className={button}>
|
||||
<Search size={16} aria-hidden="true" />
|
||||
Pesquisar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border px-4 py-2 underline"
|
||||
onClick={clear}
|
||||
>
|
||||
Limpar filtros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,19 @@ import type { InvestmentMapArea } from '@/components/interactive-investment-map'
|
||||
import { type TerritoryOption } from '@/components/investment-territory';
|
||||
import { Frame, button, card, money, api } from './shared';
|
||||
import type { Opportunity, References } from './shared';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bookmark,
|
||||
Columns2,
|
||||
Download,
|
||||
LayoutGrid,
|
||||
List,
|
||||
Map as MapIcon,
|
||||
MapPin,
|
||||
Route,
|
||||
} from 'lucide-react';
|
||||
import CatalogueFilterGroup from '@/components/catalogue-filter-group';
|
||||
import '../../../css/investment-catalogue.css';
|
||||
|
||||
type Feature = {
|
||||
geometry: { coordinates: [number, number] };
|
||||
@@ -63,6 +76,7 @@ export default function Catalogue({
|
||||
const page = usePage<{ auth: { user: unknown } }>();
|
||||
const { auth } = page.props;
|
||||
const [investmentSelection, setInvestmentSelection] = useState<number[]>(
|
||||
<<<<<<< HEAD
|
||||
readOpportunitySelection,
|
||||
);
|
||||
const [selectionStorageUnavailable, setSelectionStorageUnavailable] =
|
||||
@@ -71,11 +85,33 @@ export default function Catalogue({
|
||||
setInvestmentSelection(next);
|
||||
setSelectionStorageUnavailable(!persistOpportunitySelection(next));
|
||||
}
|
||||
=======
|
||||
() => {
|
||||
try {
|
||||
return JSON.parse(
|
||||
sessionStorage.getItem(
|
||||
'investment-opportunity-selection',
|
||||
) ?? '[]',
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
);
|
||||
>>>>>>> origin/main
|
||||
function selectInvestment(id: number, checked: boolean) {
|
||||
const next = checked
|
||||
? [...new Set([...investmentSelection, id])]
|
||||
: investmentSelection.filter((x) => x !== id);
|
||||
<<<<<<< HEAD
|
||||
updateInvestmentSelection(next);
|
||||
=======
|
||||
setInvestmentSelection(next);
|
||||
sessionStorage.setItem(
|
||||
'investment-opportunity-selection',
|
||||
JSON.stringify(next),
|
||||
);
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
const form = useForm({
|
||||
perspective: String(filters.perspective ?? ''),
|
||||
@@ -362,6 +398,7 @@ export default function Catalogue({
|
||||
};
|
||||
return (
|
||||
<Frame
|
||||
mainClassName="investment-catalogue"
|
||||
title={
|
||||
form.data.education
|
||||
? 'Oportunidades na Educação'
|
||||
@@ -379,10 +416,11 @@ export default function Catalogue({
|
||||
Iniciar ou continuar ideia nesta localização
|
||||
</Link>
|
||||
)}
|
||||
<p>
|
||||
<p className="catalogue-intro">
|
||||
Combine os filtros para encontrar oportunidades publicadas.
|
||||
Nenhum critério é alargado automaticamente.
|
||||
</p>
|
||||
<<<<<<< HEAD
|
||||
<OpportunitySearchFilters
|
||||
data={form.data}
|
||||
references={references}
|
||||
@@ -583,20 +621,61 @@ export default function Catalogue({
|
||||
'',
|
||||
) || undefined
|
||||
}
|
||||
=======
|
||||
<div className="catalogue-workspace">
|
||||
<aside aria-label="Filtros do catálogo">
|
||||
<OpportunitySearchFilters
|
||||
data={form.data}
|
||||
references={references}
|
||||
path={territoryPath}
|
||||
change={(patch) =>
|
||||
form.setData((d) => ({ ...d, ...patch }))
|
||||
}
|
||||
territory={changeTerritory}
|
||||
submit={() => runSearch()}
|
||||
errors={form.errors}
|
||||
clear={() => {
|
||||
setTerritoryPath([]);
|
||||
form.setData(
|
||||
(d) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(d).map(
|
||||
([key, value]) => [
|
||||
key,
|
||||
key === 'view'
|
||||
? 'grid'
|
||||
: key === 'sort'
|
||||
? 'recent'
|
||||
: key ===
|
||||
'territory_basis'
|
||||
? 'all'
|
||||
: Array.isArray(value)
|
||||
? []
|
||||
: typeof value ===
|
||||
'boolean'
|
||||
? false
|
||||
: '',
|
||||
],
|
||||
),
|
||||
) as unknown as typeof d,
|
||||
);
|
||||
}}
|
||||
>>>>>>> origin/main
|
||||
/>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) &&
|
||||
!error &&
|
||||
mapContext.length > 0 && (
|
||||
<section
|
||||
className={card}
|
||||
aria-label="Estruturas territoriais consultáveis"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Estruturas territoriais e projetos
|
||||
</aside>
|
||||
<section
|
||||
id="catalogue-results"
|
||||
className="catalogue-results"
|
||||
aria-label="Oportunidades publicadas"
|
||||
aria-busy={searchLoading}
|
||||
>
|
||||
<header className="catalogue-results-heading">
|
||||
<h2>
|
||||
Oportunidades publicadas
|
||||
{filters.sector && ` · ${references.sectors.find((sector) => sector.slug === filters.sector)?.name ?? filters.sector}`}
|
||||
{territorySelection.length > 0 && ` · ${territorySelection.map((territory) => territory.name).join(' / ')}`}
|
||||
</h2>
|
||||
<<<<<<< HEAD
|
||||
<p>
|
||||
Estas fichas continuam consultáveis
|
||||
independentemente de terem coordenadas. A lista não
|
||||
@@ -724,15 +803,133 @@ export default function Catalogue({
|
||||
>
|
||||
Ver ficha
|
||||
</Link>
|
||||
=======
|
||||
<SearchState
|
||||
loading={searchLoading}
|
||||
error={searchError}
|
||||
total={results.total}
|
||||
retry={() => runSearch()}
|
||||
/>
|
||||
</header>
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.total === 0 &&
|
||||
selectedStructures.length > 0 && (
|
||||
<section
|
||||
className={card}
|
||||
aria-label="Estrutura seleccionada"
|
||||
>
|
||||
<p className="font-semibold">
|
||||
Sem oportunidades publicadas associadas com
|
||||
estes filtros.
|
||||
</p>
|
||||
<p>
|
||||
A estrutura continua disponível para
|
||||
consulta. A sua selecção não confirma
|
||||
benefícios nem disponibilidade para
|
||||
investimento.
|
||||
</p>
|
||||
{selectedStructures.map((record) =>
|
||||
record.url ? (
|
||||
<Link
|
||||
key={record.id}
|
||||
className="block underline"
|
||||
href={searchTerritoryHref(
|
||||
record.url,
|
||||
page.url,
|
||||
)}
|
||||
>
|
||||
Consultar ficha: {record.name}
|
||||
</Link>
|
||||
) : (
|
||||
<p key={record.id}>
|
||||
{record.name} — sem ficha pública
|
||||
detalhada disponível.
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
<div className="catalogue-results-toolbar">
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.total === 0 &&
|
||||
(form.data.province_id ||
|
||||
form.data.district_id ||
|
||||
form.data.admin_post_id ||
|
||||
form.data.locality_id) && (
|
||||
<button
|
||||
type="button"
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
setTerritoryPath([]);
|
||||
form.setData((data) => ({
|
||||
...data,
|
||||
province_id: '',
|
||||
district_id: '',
|
||||
admin_post_id: '',
|
||||
locality_id: '',
|
||||
province: '',
|
||||
district: '',
|
||||
geo_location_id: '',
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Pesquisar noutras províncias (remover localização)
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className="catalogue-view-options"
|
||||
role="group"
|
||||
aria-label="Apresentação dos resultados"
|
||||
>
|
||||
{[
|
||||
'grid',
|
||||
'list',
|
||||
'map',
|
||||
'combined',
|
||||
'corridors',
|
||||
].map((value, i) => {
|
||||
const Icon = [
|
||||
LayoutGrid,
|
||||
List,
|
||||
MapIcon,
|
||||
Columns2,
|
||||
Route,
|
||||
][i];
|
||||
return (
|
||||
>>>>>>> origin/main
|
||||
<button
|
||||
aria-pressed={saved.includes(r.id)}
|
||||
className="underline"
|
||||
onClick={() => toggle(r.id)}
|
||||
key={value}
|
||||
disabled={
|
||||
searchLoading || !!searchError
|
||||
}
|
||||
aria-pressed={view === value}
|
||||
className="catalogue-view-button"
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams(
|
||||
query(),
|
||||
);
|
||||
params.set('view', value);
|
||||
router.get(
|
||||
`${catalogueUrl}?${params}`,
|
||||
{},
|
||||
{ preserveScroll: true },
|
||||
);
|
||||
}}
|
||||
>
|
||||
{saved.includes(r.id)
|
||||
? 'Retirar favorito'
|
||||
: 'Guardar favorito'}
|
||||
<Icon size={16} aria-hidden="true" />
|
||||
{
|
||||
[
|
||||
'Grelha',
|
||||
'Lista',
|
||||
'Mapa',
|
||||
'Lista e mapa',
|
||||
'Por corredor',
|
||||
][i]
|
||||
}
|
||||
</button>
|
||||
<<<<<<< HEAD
|
||||
<label className="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -912,25 +1109,526 @@ export default function Catalogue({
|
||||
page.url,
|
||||
)}
|
||||
key={d.id}
|
||||
className={card}
|
||||
=======
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="catalogue-utility-actions">
|
||||
<a
|
||||
className="catalogue-utility-link"
|
||||
aria-disabled={searchLoading || !!searchError}
|
||||
tabIndex={
|
||||
searchLoading || searchError
|
||||
? -1
|
||||
: undefined
|
||||
}
|
||||
onClick={(event) => {
|
||||
if (searchLoading || searchError)
|
||||
event.preventDefault();
|
||||
}}
|
||||
href={'/api/v1/investment/export?' + query()}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{d.content.code} ·{' '}
|
||||
{d.cluster_id
|
||||
? 'Cluster'
|
||||
: d.corridor_id
|
||||
? 'Corredor'
|
||||
: 'Projecto âncora'}
|
||||
</span>
|
||||
<h3 className="mt-2 font-semibold">
|
||||
{d.title}
|
||||
</h3>
|
||||
<Download size={15} aria-hidden="true" />
|
||||
Exportar CSV (até 1.000)
|
||||
</a>
|
||||
<Link
|
||||
className="catalogue-utility-link"
|
||||
href="/investidor/manifestacoes"
|
||||
>
|
||||
<Bookmark size={15} aria-hidden="true" />
|
||||
Favoritos e pedidos
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) && (
|
||||
<div
|
||||
role={error ? 'alert' : 'status'}
|
||||
aria-live="polite"
|
||||
>
|
||||
{error ? (
|
||||
<>
|
||||
Não foi possível apresentar o mapa. A
|
||||
lista continua disponível.{' '}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() =>
|
||||
setMapAttempt(
|
||||
(value) => value + 1,
|
||||
)
|
||||
}
|
||||
>
|
||||
Tentar novamente
|
||||
</button>
|
||||
</>
|
||||
) : 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.`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) &&
|
||||
!error && <Map points={points} areas={areas} />}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) && (
|
||||
<TerritoryGeometryPreview
|
||||
territoryId={
|
||||
form.data.locality_id ||
|
||||
form.data.admin_post_id ||
|
||||
form.data.district_id ||
|
||||
form.data.province_id
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) &&
|
||||
!error &&
|
||||
mapContext.length > 0 && (
|
||||
<section
|
||||
>>>>>>> origin/main
|
||||
className={card}
|
||||
aria-label="Estruturas territoriais consultáveis"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Estruturas territoriais e projetos
|
||||
</h2>
|
||||
<p>
|
||||
Estas fichas continuam consultáveis
|
||||
independentemente de terem coordenadas. A
|
||||
lista não atribui implantação às
|
||||
oportunidades nem cria pontos no mapa. Até
|
||||
30 fichas por conjunto; refine a localização
|
||||
para pesquisar.
|
||||
</p>
|
||||
<ul>
|
||||
{mapContext.map((record) => (
|
||||
<li key={record.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={record.url}
|
||||
>
|
||||
{record.kind}: {record.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) &&
|
||||
truncated && (
|
||||
<p role="alert">
|
||||
O mapa atingiu o limite de 1.000 registos;
|
||||
refine os filtros. A cobertura não está
|
||||
completa.
|
||||
</p>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
view === 'map' &&
|
||||
unlocated.length > 0 && (
|
||||
<section
|
||||
className={card}
|
||||
aria-label="Investimentos a georreferenciar"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
{unlocated.length} investimentos sem
|
||||
coordenadas de local confirmadas
|
||||
</h2>
|
||||
<p>
|
||||
Continuam nos resultados. Seleccionar uma
|
||||
província não determina a posição exacta do
|
||||
investimento.
|
||||
</p>
|
||||
<ul>
|
||||
{unlocated.map((record) => (
|
||||
<li key={record.id}>
|
||||
<Link
|
||||
href={record.url}
|
||||
className="underline"
|
||||
>
|
||||
{record.title}
|
||||
</Link>{' '}
|
||||
—{' '}
|
||||
{record.location ||
|
||||
'Localização por completar'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{view === 'corridors' && !searchLoading && !searchError && (
|
||||
<OpportunityCorridorGroups
|
||||
groups={corridorGroups}
|
||||
selected={investmentSelection}
|
||||
onSelect={selectInvestment}
|
||||
onClear={() => {
|
||||
setInvestmentSelection([]);
|
||||
sessionStorage.removeItem(
|
||||
'investment-opportunity-selection',
|
||||
);
|
||||
}}
|
||||
onCompare={(ids) =>
|
||||
void api(
|
||||
'/api/v1/investment/compare?' +
|
||||
ids
|
||||
.map((id) => 'ids[]=' + id)
|
||||
.join('&'),
|
||||
)
|
||||
.then((data) => setComparison(data.records))
|
||||
.catch((e) => setError(e.message))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{view !== 'corridors' &&
|
||||
!searchLoading &&
|
||||
!searchError &&
|
||||
(view !== 'map' || error || points?.length === 0) && (
|
||||
<div
|
||||
className={
|
||||
view === 'grid'
|
||||
? 'catalogue-cards catalogue-cards-grid'
|
||||
: 'catalogue-cards catalogue-cards-list'
|
||||
}
|
||||
>
|
||||
{results.data.map((r) => (
|
||||
<article
|
||||
key={r.id}
|
||||
className="catalogue-opportunity"
|
||||
>
|
||||
<div className="catalogue-opportunity-body">
|
||||
<p className="catalogue-sector">
|
||||
{r.sector?.name}
|
||||
</p>
|
||||
<p className="catalogue-location">
|
||||
<MapPin
|
||||
size={15}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{r.territorial_summary || [r.province, r.district].filter(Boolean).join(' / ') ||
|
||||
'Localização não indicada'}
|
||||
</span>
|
||||
</p>
|
||||
<h2 className="text-xl font-bold">
|
||||
<Link
|
||||
href={`/oportunidades/${r.slug}${query() ? `?${query()}` : ''}`}
|
||||
>
|
||||
{r.title}
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="catalogue-summary">
|
||||
{r.summary}
|
||||
</p>
|
||||
<div className="catalogue-opportunity-facts">
|
||||
<p className="font-semibold">
|
||||
{money(r)}
|
||||
</p>
|
||||
<p className="catalogue-maturity">
|
||||
{r.proactive
|
||||
? r.proactive.group ===
|
||||
'ideas'
|
||||
? 'Ideia em preparação'
|
||||
: r.proactive
|
||||
.group ===
|
||||
'needs'
|
||||
? 'Necessidade identificada'
|
||||
: 'O que existe'
|
||||
: r.maturity_label}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="catalogue-opportunity-actions">
|
||||
<div className="catalogue-opportunity-links">
|
||||
<Link
|
||||
className="catalogue-detail-link"
|
||||
href={`/oportunidades/${r.slug}${query() ? `?${query()}` : ''}`}
|
||||
>
|
||||
Ver ficha
|
||||
<ArrowUpRight
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
aria-pressed={saved.includes(
|
||||
r.id,
|
||||
)}
|
||||
className="catalogue-favourite"
|
||||
onClick={() => toggle(r.id)}
|
||||
>
|
||||
<Bookmark
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
fill={
|
||||
saved.includes(r.id)
|
||||
? 'currentColor'
|
||||
: 'none'
|
||||
}
|
||||
/>
|
||||
{saved.includes(r.id)
|
||||
? 'Retirar favorito'
|
||||
: 'Guardar favorito'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="catalogue-opportunity-choices">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={investmentSelection.includes(
|
||||
r.id,
|
||||
)}
|
||||
onChange={(e) =>
|
||||
selectInvestment(
|
||||
r.id,
|
||||
e.target
|
||||
.checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
Escolher para preparar
|
||||
investimento
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(
|
||||
r.id,
|
||||
)}
|
||||
disabled={
|
||||
!selected.includes(
|
||||
r.id,
|
||||
) &&
|
||||
selected.length >= 4
|
||||
}
|
||||
onChange={(e) =>
|
||||
setSelected(
|
||||
e.target.checked
|
||||
? [
|
||||
...selected,
|
||||
r.id,
|
||||
]
|
||||
: selected.filter(
|
||||
(
|
||||
id,
|
||||
) =>
|
||||
id !==
|
||||
r.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
Comparar
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!searchLoading && !searchError && !results.total && (
|
||||
<p className={card}>
|
||||
Não há oportunidades publicadas com estes filtros no cadastro consultado.
|
||||
A informação disponível pode ser incompleta. Pode{' '}
|
||||
<Link href="/apoio" className="underline">
|
||||
pedir apoio
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<nav
|
||||
aria-label="Paginação"
|
||||
className={
|
||||
view === 'corridors'
|
||||
? 'hidden'
|
||||
: 'catalogue-pagination'
|
||||
}
|
||||
>
|
||||
{results.prev_page_url && (
|
||||
<Link href={results.prev_page_url}>Anterior</Link>
|
||||
)}
|
||||
{results.next_page_url && (
|
||||
<Link href={results.next_page_url}>Seguinte</Link>
|
||||
)}
|
||||
</nav>
|
||||
{selected.length > 0 && (
|
||||
<button
|
||||
className={button}
|
||||
onClick={() =>
|
||||
api(
|
||||
'/api/v1/investment/compare?' +
|
||||
selected
|
||||
.map((id) => 'ids[]=' + id)
|
||||
.join('&'),
|
||||
)
|
||||
.then((data) => setComparison(data.records))
|
||||
.catch((e) => setError(e.message))
|
||||
}
|
||||
>
|
||||
Comparar {selected.length} oportunidades
|
||||
</button>
|
||||
)}
|
||||
{view !== 'corridors' && !!investmentSelection.length && (
|
||||
<section className={card}>
|
||||
<p>
|
||||
{investmentSelection.length} oportunidades
|
||||
escolhidas, conservadas ao mudar de página.
|
||||
</p>
|
||||
<Link
|
||||
className={button}
|
||||
href={
|
||||
'/investimentos/iniciar?' +
|
||||
investmentSelection
|
||||
.map((id) => 'opportunities[]=' + id)
|
||||
.join('&')
|
||||
}
|
||||
>
|
||||
Preparar investimento com as oportunidades
|
||||
escolhidas
|
||||
</Link>
|
||||
<button
|
||||
className="ml-4 underline"
|
||||
onClick={() => {
|
||||
setInvestmentSelection([]);
|
||||
sessionStorage.removeItem(
|
||||
'investment-opportunity-selection',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Limpar escolha
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
{comparison.length > 0 && (
|
||||
<div ref={comparisonRef} className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<caption className="p-3 text-left font-bold">
|
||||
Comparação — montantes nas moedas de origem
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border p-3">Campo</th>
|
||||
{comparison.map(({ record: r }) => (
|
||||
<th
|
||||
key={r.id}
|
||||
className="border p-3"
|
||||
>
|
||||
{r.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
'Sector',
|
||||
'Província',
|
||||
'Investimento',
|
||||
'Maturidade',
|
||||
'Parceria',
|
||||
'Impacto',
|
||||
].map((field, i) => (
|
||||
<tr key={field}>
|
||||
<th className="border p-3">
|
||||
{field}
|
||||
</th>
|
||||
{comparison.map(({ record: r }) => (
|
||||
<td
|
||||
className="border p-3"
|
||||
key={r.id}
|
||||
>
|
||||
{[
|
||||
r.sector?.name,
|
||||
r.province,
|
||||
money(r),
|
||||
r.maturity_label,
|
||||
r.financing_modality,
|
||||
r.expected_impact,
|
||||
][i] || 'Não indicado'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{territorial.length > 0 && (
|
||||
<section
|
||||
className="catalogue-territorial"
|
||||
aria-label="Contexto territorial e projectos âncora"
|
||||
>
|
||||
<h2 className="text-2xl font-bold">
|
||||
Contexto territorial e projectos âncora
|
||||
</h2>
|
||||
<p>
|
||||
Fichas do documento de referência; montantes e
|
||||
metas conservam a natureza declarada na fonte.
|
||||
</p>
|
||||
{[
|
||||
{
|
||||
title: 'Corredores',
|
||||
records: territorial.filter(
|
||||
(d) => !d.cluster_id && d.corridor_id,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Clusters',
|
||||
records: territorial.filter(
|
||||
(d) => d.cluster_id,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Projectos âncora',
|
||||
records: territorial.filter(
|
||||
(d) => !d.cluster_id && !d.corridor_id,
|
||||
),
|
||||
},
|
||||
]
|
||||
.filter((group) => group.records.length)
|
||||
.map((group) => (
|
||||
<CatalogueFilterGroup
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
count={group.records.length}
|
||||
>
|
||||
<div className="catalogue-territorial-links">
|
||||
{group.records.map((d) => (
|
||||
<Link
|
||||
href={searchTerritoryHref(
|
||||
`/carteira/${d.id}`,
|
||||
page.url,
|
||||
)}
|
||||
key={d.id}
|
||||
className="catalogue-territorial-link"
|
||||
>
|
||||
<span className="text-sm">
|
||||
{d.content.code} ·{' '}
|
||||
{d.cluster_id
|
||||
? 'Cluster'
|
||||
: d.corridor_id
|
||||
? 'Corredor'
|
||||
: 'Projecto âncora'}
|
||||
</span>
|
||||
<h3 className="mt-2 font-semibold">
|
||||
{d.title}
|
||||
</h3>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</CatalogueFilterGroup>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
<CatalogueGuideLinks articles={guides} />
|
||||
</section>
|
||||
)}
|
||||
<CatalogueGuideLinks articles={guides} />
|
||||
</div>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user