PORTAL DO INVESTIDOR
INVESTE Moçambique
Índice
​
Índice
2
Resumo executivo
4
1.
Pilares de conteúdo e serviços
5
2.
Integração institucional e interoperabilidade
5
3.
Experiência do utilizador e acessibilidade
5
4.
Plano director por vertente
6
5.
Integração técnica com APIs governamentais
7
Especificações dos endpoints-chave
7
6.
Alinhamento Jurídico com a Lei de Investimentos Privados
8
Diagnóstico do portal actual.
8
1.
O que o portal já faz bem (Pontos Fortes)
8
Melhorias necessárias no portal actual
9
1.
Incoerências de conteúdo e experiência
9
2.
Transição para portal transaccional
9
3.
Alinhamento jurídico e regulatório
9
4.
Recursos multilingues e suporte
9
Recomendação geral
10
Implementação por prioridades
10
Roteiro técnico-funcional de implementação
11
Fase 1: arquitectura do sistema e segurança
11
Fase 2: integrações por API
11
Fase 3: componentes e funcionalidades de UI/UX
12
Fase 4: reorganização da página principal
13
Ficha-modelo de oportunidade de investimento
13
a)
Sumário rápido da ficha
13
b)
Objectivo
14
I.
Tabela de identificação da oportunidade
14
c)
Orientação para o resumo executivo
14
II.
Tabela de estágio de maturidade
14
III.
Tabela de estrutura financeira
14
IV.
Tabela de impacto esperado
15
d)
Requisitos, documentos e validação
15
e)
Modelo resumido para card da página principal
15
f)
Instruções de preenchimento
15
Anexo executivo: ciclo de vida do investidor
17
V.
Matriz de etapas, responsáveis e resultados
18
VI.
Gráficos de desempenho
19
VII.
Análise de tendências
20
VIII.
Gráficos comparativos históricos
21
IX.
Recomendações estratégicas
22
Indicadores de risco
23
Bolsa de parcerias, investidores e capital.
24
Validação automática dos formulários
27
Formulário de inscrição: investidores
28
Formulário de inscrição: empresas e promotores
29
Declaração de consentimento e confidencialidade
29
Página Principal
30
Comparação de oportunidades
32
Organização da página principal do portal
35
Modelo de ficha para oportunidades de investimento
37
Resumo 
Este documento propõe a evolução do Portal do Investidor da Tempo
,
 Lda
.
 
P
ara uma plataforma digital de apoio à implementação do 
programa nacional
 de 
investimentos.
 As recomendações concentram-se em cinco prioridades:
Alinhar conteúdos e serviços com os eixos estratégicos 
para investimentos
;
Organizar oportunidades por sectores, 
prov
í
ncias
 
s
e cadeias de valor;
Reforçar a credibilidade jurídica e institucional;
Melhorar a experiência do utilizador;
Preparar funcionalidades transaccionais para consulta de oportunidades, simulação de incentivos, acesso à legislação essencial e acompanhamento integrado de processos de investimento.
Este documento tem como 
objectivo
 apoiar a melhoria do Portal do Investidor da Tempo 
Lda.
, disponível em https://infopublica.tempo.co.mz/portal-do-investidor, identificando 
correcções
 de conteúdo, ajustes 
na
 experiência do utilizador e funcionalidades prioritárias para tornar a plataforma mais útil, credível e orientada 
para a
s necessidades de investidores nacionais e estrangeiros.
O portal deve funcionar como vitrine digital, mecanismo de facilitação e instrumento de acompanhamento das prioridades do 
investimento identificadas pelo governo
, apoiando a mobilização de investimento público, privado e em parceria público-privada nos sectores estratégicos definidos para o período 2026-2030.
Carteira de 
projectos
 prioritários: 
Organizar oportunidades por sector, província, estágio de maturidade, entidade promotora, modalidade de financiamento e impacto esperado.
Promoção territorial do investimento:
 destacar oportunidades provinciais e distritais, incluindo corredores logísticos, zonas económicas especiais, zonas francas industriais e polos produtivos.
Facilitação e acompanhamento
: permitir que 
os 
investidores 
identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais
 de contacto para cada oportunidade.
Programa de
 Investimentos 
2026-2030
:
 incluir indicadores de execução, tais como 
o número de oportunidades publicadas, as manifestações de interesse recebidas, os projectos em análise, o montante estimado de investimento mobilizado e o estado
 de implementação por sector.
O
s eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:
Pilares de conteúdo e serviços
Guia Regulatório e de Licenciamento:
Integração das normas de 
l
icenciamento.
Fluxogramas 
que clarificam
 o passo a passo para a abertura de empresas e
 o
 registo de investimentos através da APIEX (Agência para a Promoção de Investimento e Exportações).
Sectores Estratégicos e Oportunidades Prioritárias:
Apresentação clara de 
projectos
 nas áreas-chave impulsionadas pelas políticas públicas: Agronegócio, Energia & Mineração (com foco em energias renováveis e na transição energética), Turismo, Infraestruturas e Mercado Financeiro / Economia Azul.
Quadro Fiscal, Incentivos e Repatriamento de Capitais:
Detalhe das taxas reduzidas de IRPC (ex
.
: 10% para 
a 
agricultura e 
os 
transportes) e
 das
 isenções de IVA para insumos produtivos.
Explicação clara do regime legal de repatriamento de capitais e 
das 
normas cambiais do Banco de Moçambique.
Zonas Económicas Especiais (ZEE) e Zonas Francas Industriais (ZFI):
Mapeamento 
interactivo
 e explicativo 
d
os benefícios fiscais e aduaneiros específicos das ZEEs e ZFIs em Moçambique.
Integração institucional e interoperabilidade
O portal deve servir como um balcão ou agregador facilitador, fazendo a ponte 
directa
 com os ecossistemas governamentais oficiais:
APIEX:
 Registo e aprovação de 
projectos
 de investimento privado.
Autoridade Tributária (AT):
 Obtenção de NUIT, informação sobre benefícios fiscais e procedimentos aduaneiros.
Banco de Moçambique:
 Registo de capital estrangeiro e regulamentação cambial.
Bolsa de Valores de Moçambique (BVM) / Bolsa de Mercadorias:
 Oportunidades no mercado financeiro e 
na 
comercialização agrícola.
e
-SNIP / MEF:
 Transparência sobre 
projectos
 públicos e infraestruturas estratégicas.
Experiência do utilizador e acessibilidade
Princípios de experiência do utilizador:
 O portal deve ser desenhado a partir das principais tarefas do investidor: 
compreender oportunidades, 
comparar sectores, 
confirmar requisitos legais, 
simular incentivos, 
I
niciar contacto e acompanhar processos. 
Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de 
acção
 claras e oferecer um caminho visível para apoio especializado.
Calculadora ou simulador de incentivos:
 permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional.
Suporte multil
í
ngue:
 disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de 
acção
 e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de man
ter
 os conteúdos.
Área Reservada 
ou 
Dashboard do Investidor
:
 permitir ao investidor acompanhar o
 estado das submissões, 
visualizar documentos pendentes, receber notificações, agendar 
reuniões virtuais
, consultar histórico de 
interacções
 e solicitar
 assistência 
direta
 junto dos serviços de facilitação do investimento.
Requisitos mínimos de acessibilidade:
 O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio con
tínuo
.
Plano director por vertente
1. 
Arquitectura
 de Informação e Conteúdos (
Sitemap
 & UX)
A navegação deve 
reflectir
 o ciclo de vida do investidor: 
descoberta de oportunidades, 
análise de requisitos, 
preparação e submissão do 
projecto
,
acompanhamento do processo 
E
 apoio pós-investimento.
A. Áreas Principais do Portal
a) 
Por
 que
 Moçambique?
Dados macroeconómicos 
actualizados
 e relatórios de risco de país.
Localização estratégica (acesso ao mercado da SADC e 
aos 
corredores 
logísticos
).
b) 
Guia do Investidor (Passo a Passo):
Mapeamento do percurso legal:
 Constituição de empresa 
 Obtenção de NUIT 
 Registo na APIEX 
 Licenciamento e Abertura de Conta Bancária.
c) 
Sectores e 
Projectos
 Estratégicos:
Catálogo 
interactivo
 por sector (Energia e Mineração, Agronegócio, Turismo, Infraestruturas e Mercado Financeiro).
d) Carteira PII 2026-2030:
 cada sector deve apresentar oportunidades associadas ao 
PII 2026-2030
, com indicação 
da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos
 de referência disponíveis.
e)
 
Oportunidades em Zonas Económicas Especiais (ZEE) e Zonas Francas Industriais (ZFI).
Quadro Legal & Incentivos:
Repositório documental 
descarregável
 (Leis, Decretos, Regulamentos Cambiais).
Guia de tributação e 
de 
facilidades fiscais.
Balcão Virtual / Área Reservada:
Submissão e acompanhamento de propostas de investimento em tempo real.
Integração técnica com APIs governamentais
Para transformar o portal num balcão único interoperável, as integrações de sistema devem ser tratadas como uma ambição funcional dependente de acordos institucionais, 
da 
disponibilidade técnica das entidades públicas e
 da
 validação jurídica dos fluxos de dados.
[Investidor
 / 
Portal]
 
       │
       
├
──
►
 APIEX (API de Registo) ──
►
 Validação de 
Projetos
 & Licenciamento
       
├
──
►
 Autoridade Tributária ──
►
 Emissão de NUIT & Benefícios Fiscais
       
├
──
►
 Banco de Moçambique  ──
►
 Verificação Cambial & Repatriamento
       └──
►
 BRC (Registo Comercial)─
►
 Verificação de Entidades Singulares/
Coletivas
Especificações dos endpoints-chave
APIEX (Agência de Promoção de Investimentos e Exportações):
Submissão 
electrónica
 do Formulário de Pedido de Autorização de Investimento.
Consulta de estado do processo via 
Webhook
 de notificação.
Tributária (AT):
Verificação e atribuição automática do NUIT de investidor singular ou 
coletivo
.
Emissão e validação de 
c
ertidões de 
q
uitação e
 de
 elegibilidade a benefícios fiscais.
Banco de Moçambique (BM):
Integração com a plataforma de registo de Operações Cambiais de Capital (para registo do investimento estrangeiro e garantia de repatriamento de lucros/dividendos).
Segurança e Autenticação:
Autenticação via 
OAuth
 2.0 / 
OpenID
 
Connect
.
Criptografia de dados ponta a ponta (TLS 1.3 / AES-256) em conformidade com as normas estatais de 
protecção
 de dados.
Alinhamento Jurídico com a Lei de Investimentos Privados
O conteúdo e os fluxos de trabalho do portal devem 
refletir
 rigorosamente o quadro legal vigente em Moçambique:
Lei de Investimentos e Regulamentação:
Alinhamento 
directo
 com as garantias legais de 
protecção
 da propriedade privada e 
de não 
discriminação entre investidores nacionais e estrangeiros.
Clareza 
sobre 
os limites mínimos de capital para elegibilidade a incentivos específicos do regime de investimento privado.
Facilitação Cambial e Repatriamento:
Instruções claras sobre a obrigatoriedade de registo do capital de entrada junto do Banco de Moçambique
,
 para assegurar o direito 
à
 transferência de lucros, dividendos e capital investido de volta ao país de origem.
Incentivos Fiscais e Aduaneiros:
Incorporação das reduções e isenções previstas 
(ex.: redução das taxas de IRPC para a agricultura e os transportes;
 isenções de direitos aduaneiros para bens de equipamento da Classe K).
Simplificação de Processos:
Mecanismo de 
m
era Comunicação Prévia para 
actividades
 de baixo risco económico, eliminando burocracias e reduzindo 
os 
prazos de resposta das entidades licenciadoras.
No p
ortal 
actual
 em 
INVESTE Moçambique
,
 
a estrutura existente serve como uma boa base conceptual e demonstrativa, mas precisa de melhorias significativas para se tornar uma ferramenta plenamente funcional, fiável e 
atractiva
 para investidores nacionais e estrangeiros.
Diagnóstico do portal actual
.
O que o portal já faz bem (Pontos Fortes)
Estrutura Visual Limpa:
 Apresentação moderna, com bom contraste e organização clara 
em
 blocos de conteúdo.
Sectores Bem Mapeados:
 A divisão em áreas
-
chave (Energia & Mineração, Agronegócio, Turismo, Infraestruturas e Mercado Financeiro) alinha-se com as prioridades económicas reais do país.
Mapeamento Institucional:
 A indicação visível dos principais reguladores (APIEX, Banco de Moçambique, Autoridade Tributária, BVM e Bolsa de Mercadorias) transmite 
corretamente
 
o ecossistema institucional.
Melhorias necessárias no portal actual
Incoerências de conteúdo e experiência
Barra de Pesquisa Descontextualizada:
 O campo de busca deve sugerir temas 
directamente
 ligados ao investimento, como “registo de 
projecto
 na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania.
Falta de Profundidade nos Cards:
 As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo.
Transição para portal transaccional
Falta de Integração Operacional: Os logos dos reguladores e os botões da página funcionam apenas como links estáticos. Para um investidor, é crucial ter balcão único, onde possa:
Submeter propostas ou pré-candidaturas de 
projectos
 
directamente
 à APIEX.
Consultar tabelas 
interactivas 
de incentivos fiscais por província e sector.
Iniciar/consultar pedidos de NUIT ou registos operacionais.
Alinhamento jurídico e regulatório
Quadro Legal Acessível:
 Falta um repositório claro de documentos jurídicos essenciais para descarregar, como a Nova Lei de Investimentos Privados, o regulamento das Zonas Económicas Especiais (ZEE) e o guia de Regulamentação Cambial do Banco de Moçambique para
 o
 repatriamento de capitais.
Guias de processos passo a passo:
 deve existir uma secção dedicada que explique, de forma visual e textual, a rota do investidor: constituição d
a
 empresa, obtenção de NUIT, registo na APIEX, licenciamento simplificado
,
 quando aplicável, abertura de conta bancária e registo cambial.
Recursos multilingues e suporte
Idioma:
 Sendo Moçambique um polo de 
atração
 de investimento internacional, o portal 
necessita, obrigatoriamente, de alternância fluida entre português e inglês (e, idealmente,
 
francês
 ou 
m
andarim).
Canais de Suporte 
Directo
:
 Além do contacto genérico por e-mail
 ou 
telefone, seria muito vantajoso ter um sistema de agendamento de reuniões virtuais com assistentes da APIEX ou um 
helpdesk
 
dedi
cado 
a
o investidor.
Nota de validação:
 Todas as referências a benefícios fiscais, taxas reduzidas, incentivos aduaneiros, repatriamento de capitais e licenciamento 
devem
 ser confirmadas com a legislação vigente e com as entidades competentes antes da publicação final no portal. O portal deve apresentar informaç
ões
 orientadora
s
, mas 
deve 
evitar substituir parecer jurídico, decisão administrativa ou confirmação oficial das autoridades.
Correcções
 imediatas recomendadas na página 
actual
: 
substituir exemplos de pesquisa ligados 
à
 cidadania por termos de investimento; 
rever todos os cards 
sectoriais
 para eliminar textos repetidos; 
criar chamadas claras para documentos legais, incentivos e procedimentos; 
incluir uma opção visível de idioma; 
E
 acrescentar uma secção “Como começar” com passos simples para novos investidores.
Como começar:
 A página principal deve incluir uma secção curta com cinco passos para novos investidores: 
identificar o sector de interesse, 
consultar requisitos legais e incentivos aplicáveis, 
preparar documentos básicos, 
solicitar orientação ou reunião de facilitação 
E
 iniciar o processo de registo ou 
de 
submissão junto da entidade competente.
Recomendação geral
O portal 
actual
 funciona como uma boa base visual e informativa, mas deve evoluir por etapas para se tornar uma plataforma de apoio 
ao investimento:
P
rimeiro, corrigindo textos, pesquisa, navegação, 
bilinguidade
 e conteúdos 
sectoriais
; 
depois, adicionando carteira de 
projectos
, recursos legais, guias práticos, filtros territoriais e ferramentas de apoio à decisão; 
E
, numa fase mais avançada, integrando funcionalidades 
transaccionais
, monitori
zação
 de investimentos e ligação operacional 
a
 sistemas públicos, sempre mediante validação institucional.
Implementação por prioridades
Prioridade alta:
 alinhar a página inicial com os 
objectivos
 do 
programa de investimento nacional
, corrigir a barra de pesquisa, substituir 
os 
textos genéricos dos cards, criar páginas 
sectoriais
 e territoriais com oportunidades verificáveis e inserir uma secção visível de legislação, incentivos e carteira de 
projectos
 prioritários. 
Prioridade média:
 implementar 
um 
selector
 de idiomas, melhorar
 os
 filtros de pesquisa por sector, província, entidade competente e fase do investimento, criar
 um
 formulário de contacto especializado e estruturar o repositório documental. 
Prioridade futura:
 desenvolver 
a 
Área Reservada do Investidor,
 um
 simulador fiscal orientador, acompanhamento de processos, indicadores de monitoria do 
programa de investimento nacional 
e integrações 
transaccionais
 com entidades públicas, sempre mediante validação institucional.
Para transformar o portal 
actual
 numa Plataforma Integrada de Serviços de Investimento (Single 
Window
 for 
Investment
), a implementação deve ser feita em fases estruturadas, 
abrange
ndo a 
arquitectura
 do sistema, os fluxos de integração 
via
 API e os novos componentes de interface (UI/UX).
Roteiro técnico-funcional de implementação
Fase 1: arquitectura do sistema e segurança
Antes de conectar as APIs governamentais, é necessário estabelecer uma camada intermédia de orquestração (API Gateway) no portal para garantir segurança, registo de auditoria e alta disponibilidade.
[ Frontend
: React / 
Next.js ]
            │
            
▼
 (HTTPS / OAuth 2.0)
[ 
Backend
 / API Gateway 
Integrador]
 
◄
──
►
 
[ Base
 de Dados 
Transaccional
 (PostgreSQL
) ]
            │
            
├
──
►
 API APIEX (Propostas & Licenciamento)
            
├
──
►
 API Autoridade Tributária (NUIT & Isenções)
            
├
──
►
 API Banco de Moçambique (Registo Cambial)
            └──
►
 API Conservatória / BRC (Validação Empresarial)
Gestão de identidade e autenticação
Criar o Dashboard do Investidor com login seguro (Single 
Sign-On
).
Suporte 
à
 submissão de documentos assinados digitalmente ou validados 
por
 
meio de 
verificação de identidade (KYC).
Base de dados transaccional
Implementar uma base de dados relacional para gerir o estado dos pedidos (Draft, Submetido, Em Análise na APIEX, Aprovado, Pendente de Pagamento).
Fase 2: integrações por API
A tabela seguinte apresenta exemplos conceptuais de pontos de integração que devem ser validados com cada entidade pública antes de qualquer desenvolvimento em ambiente de produção.
Nota técnica: Os endpoints indicados na tabela são exemplos conceptuais 
de
 especificação funcional e devem ser confirmados com cada entidade competente antes de qualquer implementação técnica ou
 de
 publicação como integração oficial.
Entidade Governamental
Ponto de Integração (
Endpoint
)
Função no Portal
APIEX
/
api
/v1/
investments
/
submit
Submissão 
directa
 do Formulário de Pedido de Autorização de Investimento Privado.
Autoridade Tributária (AT)
/
api
/v1/tax/nuit-validation
Validação automática do NUIT e emissão do comprovativo de enquadramento nos incentivos do Código dos Benefícios Fiscais.
Banco de Moçambique (BM)
/
api
/v1/
forex
/
register
-capital
Registo preliminar de intenção de entrada de capital estrangeiro para garantia de repatriamento de lucros.
Registo Comercial (BRC)
/
api
/v1/
company
/
verify
Verificação do estado de constituição e reserva de nome da entidade jurídica.
Fase 3: componentes e funcionalidades de UI/UX
Para resolver as limitações da versão 
actual
, o 
frontend
 deve incorporar os seguintes módulos:
Repositório digital de guias jurídicos
Secção "Centro de Recursos Legal":
 Um hub organizado por categorias com 
PDFs
 e documentos oficiais descarregáveis e pesquisáveis:
Nova Lei de Investimentos Privados e Regulamento.
Código dos Benefícios Fiscais e Quadro do PAE (Licenciamento Simplificado).
Avisos e Normativos Cambiais do Banco de Moçambique.
Filtros 
inteligentes: permitir
 filtrar documentos por sector, tipo de incentivo fiscal ou aduaneiro
,
 província
, entidade competente, fase do investimento e data de 
actualização
.
Simulador interactivo de incentivos fiscais
Uma ferramenta onde o investidor insere:
Volume de investimento proposto (ex
.
: USD).
Sector de 
actividade
 (ex
.
: Agronegócio, Energias Renováveis).
Localização (Província / ZEE / ZFI).
Resultado instantâneo: O portal calcula a estimativa d
a
 taxa de IRPC aplicável, 
os 
anos de isenção de IVA e
 a
 redução de direitos aduaneiros 
aplicável a
 bens de capital (Classe K).
Balcão virtual e acompanhamento de processos
Substituir os botões estáticos por um Acompanhador de Estado (
Tracking
 
System
):
O i
nvestidor recebe um Número de Acompanhamento Único.
Visualiza em tempo real as etapas: Análise de Viabilidade
,
 Parecer das Entidades 
Setoriais
,
 Aprovação do Projeto
 e
 Emissão do Certificado de Investimento.
Fase 4: reorganização da página principal
Com base na versão 
actual
 em 
Portal do Investidor
:
Ajustar a Barra de Pesquisa:
Alterar os exemplos de busca para termos do ecossistema de negócios: "Isenção de IVA 
na 
agricultura", "Como registar
 um
 
projecto
 na APIEX", "Repatriamento de dividendos".
Substituir os textos genéricos dos cards por dados operacionais verificáveis em cada sector, incluindo oportunidades concretas, localização, incentivos aplicáveis, entidades competentes, documentos necessários e 
uma 
chamada de 
acção
 específica para iniciar o processo.
Em vez de “Explore o potencial...”, apresentar
, quando validados,
 dados operacionais de cada sector, como área disponível, incentivos aplicáveis, entidades competentes e chamada de 
acção
 específica. Qualquer número ou taxa deve ser identificado como exemplo ilustrativo ou confirmado por fonte oficial antes da publicação.
Adicionar 
Selector
 de Idiomas Visível:
Adicionar alternador de idioma no cabeçalho (PT | EN).
Próximo Passo Recomendado
Próximo passo recomendado:
 priorizar as melhorias de conteúdo e 
de 
navegação da página pública, po
is
 são as que têm impacto imediato na credibilidade do portal. Em seguida, deve ser preparada a especificação funcional da Área Reservada do Investidor e, numa fase posterior, o desenho técnico das integrações 
via
 API com 
as 
entidades públicas relevantes.
Ficha-modelo de oportunidade de investimento
Sumário rápido da ficha
A ficha-modelo organiza cada oportunidade em: resumo rápido, objectivo, identificação, resumo executivo, maturidade, estrutura financeira, impacto, requisitos e validação, card para a página principal e instruções de preenchimento. Use-a para publicar oportunidades comparáveis, verificáveis e orientadas 
para 
o investidor.
Secção
Finalidade
Prioridade
Resumo rápido
Visão imediata da oportunidade e dos campos obrigatórios.
Obrigatória
Identificação
Classificar sector, localização, promotor, referência e estado editorial.
Obrigatória
Maturidade, finanças e impacto
Permitir comparação por preparação, necessidade de financiamento e resultados esperados.
Alta
Requisitos, card e instruções
Validar informação, gerar card público e orientar preenchimento consistente.
Obrigatória
Objectivo
Este modelo padroniza a apresentação de oportunidades no Portal do Investidor, permitindo comparar projectos por sector, província, estágio de maturidade, entidade promotora, modalidade de financiamento e impacto esperado.
Tabela de identificação da oportunidade
Campo
Informação a preencher
Nome da oportunidade
[Designação clara e comercial do projecto]
Código de referência
[Ex.: INV-MZ-AGR-001]
Sector
[Agronegócio, energia, turismo, infraestruturas, indústria, economia azul, serviços financeiros, etc.]
Subsector
[Ex.: processamento agrícola, energia solar, logística, hotelaria]
Província e distrito
[Localização principal da oportunidade]
Entidade promotora
[Governo, município, empresa pública, empresa privada, cooperativa, PPP ou parceiro institucional]
Estado editorial
[Activa, em revisão, em negociação, suspensa ou concluída]
Orientação para o resumo executivo
Apresentar, em 5 a 8 linhas, o objectivo do projecto, a localização, a necessidade de investimento, o tipo de parceiro pretendido e o impacto económico esperado. O resumo deve ser claro, directo e adequado a investidores nacionais e estrangeiros.
Modelo de texto:
 A oportunidade consiste em [descrição curta do projecto], localizada em [província/distrito], com potencial para [benefício económico ou social]. O projecto procura [tipo de financiamento/parceria] para [objectivo do investimento], 
com o objectivo de
 gerar impacto em [emprego, exportações, desenvolvimento local, receitas fiscais ou sustentabilidade].
Tabela de estágio de maturidade
Nível
Estágio
Significado
1
Ideia identificada
Oportunidade ainda conceptual, com informação preliminar limitada.
2
Pré-estudo disponível
Existe informação inicial sobre localização, sector e potencial económico.
3
Estudo de viabilidade em curso
Análises técnicas, económicas ou ambientais em preparação.
4
Projecto estruturado
Dados técnicos, financeiros e institucionais disponíveis para análise.
5
Pronto para investimento
Dossier completo e validado para apresentação a investidores.
6
Em negociação ou implementação
Oportunidade já em contacto com investidores ou em fase de execução.
Tabela de estrutura financeira
Campo
Informação a apresentar
Valor estimado do investimento
[MZN ou USD]
Financiamento pretendido
[Capital, crédito, PPP, concessão, co-financiamento, doação, leasing ou financiamento misto]
Participação pretendida
[Minoritária, maioritária, 
joint
 venture, parceria técnica ou operação delegada]
Horizonte de retorno esperado
[Curto, médio ou longo prazo]
Documentos financeiros disponíveis
[Plano de negócios, orçamento, estudo de viabilidade, projecções financeiras]
Tabela de impacto esperado
Dimensão
Indicador sugerido
Emprego
[Número estimado de postos de trabalho directos e indirectos]
Investimento mobilizável
[Montante total esperado]
Exportações
[Potencial de exportação ou acesso a mercados externos]
Substituição de importações
[Produtos ou serviços que poderão ser produzidos localmente]
Desenvolvimento local
[Benefícios para comunidades, PME, produtores locais ou municípios]
Sustentabilidade
[Impacto ambiental positivo, eficiência energética ou uso sustentável de recursos]
Receitas públicas
[Potencial de contribuição fiscal e económica]
Requisitos, documentos e validação
Licenças ou autorizações necessárias.
Instituições competentes envolvidas.
Requisitos ambientais, fundiários, fiscais, cambiais ou sectoriais.
Documentos disponíveis: perfil do projecto, estudo preliminar, estudo de viabilidade, plano de negócios, mapas, licenças e informação financeira.
Entidade responsável pela validação da informação.
Data da publicação e da última actualização.
Nota de validação: a informação legal, fiscal e regulatória deve ser tratada como orientação preliminar e confirmada junto das entidades competentes antes da publicação final ou 
da 
tomada de decisão pelo investidor.
Modelo resumido para card da página principal
[Nome do projecto]
Sector
: [Sector]
Província
: [Província/Distrito
]
Investimento
 estimado: [Valor
]
Maturidade
: [Nível/estágio
]
Financiamento
 pretendido: [Modalidade
]
Impacto
 esperado: [Emprego, exportações, desenvolvimento local, receitas fiscais ou sustentabilidade
]
Acções
: Ver ficha | Manifestar interesse | Solicitar informação
Instruções de preenchimento
A ficha deve ser preenchida com informação objectiva, verificável e suficiente para permitir que o investidor compreenda rapidamente a natureza da oportunidade, o seu grau de preparação, as necessidades de financiamento e os impactos esperados. Sempre que um campo ainda não tenha informação confirmada, deve ser assinalado como “em validação” ou “não disponível”, evitando estimativas sem fonte
.
Regras gerais
Usar linguagem clara, profissional e orientada ao investidor, evitando o 
us
o excessivo
 de jargão técnico
.
Preencher todos os campos obrigatórios antes da publicação da oportunidade.
Indicar sempre a fonte da informação, a entidade responsável e a data da última actualização.
Apresentar valores monetários 
e
m moeda explícita, preferencialmente em MZN e/ou USD.
Distinguir dados confirmados, estimados e ainda em validação.
Evitar promessas de retorno financeiro não comprovadas ou afirmações sem base documental.
Garantir coerência entre o resumo executivo, os dados financeiros, a maturidade do projecto e os documentos disponíveis.
Orientações por campo
Campo
Como preencher
Observação de qualidade
Nome da oportunidade
Usar uma designação curta, específica e compreensível.
Evitar nomes genéricos como “Projecto agrícola” sem localização ou finalidade.
Sector e subsector
Seleccionar o sector principal e indicar o subsector quando aplicável.
O sector deve permitir filtragem correcta no portal.
Província e distrito
Indicar a localização exacta ou predominante da oportunidade.
Quando houver várias localizações, mencionar todas ou indicar “multiterritorial”.
Entidade promotora
Identificar a entidade responsável pela proposta ou informação publicada.
Incluir tipo de entidade: pública, privada, municipal, cooperativa, PPP ou parceiro institucional.
Resumo executivo
Explicar finalidade, localização, necessidade de investimento e impacto esperado.
Deve permitir compreensão rápida por um investidor que ainda não conhece o projecto.
Estágio de maturidade
Seleccionar o nível que melhor descreve o estado real do projecto.
Não classificar como “pronto para investimento” sem dossier mínimo validado.
Valor estimado
Indicar montante previsto e moeda utilizada.
Assinalar se o valor é estimado, preliminar ou validado por estudo.
Modalidade de financiamento
Indicar capital, crédito, PPP, concessão, co-financiamento, doação, leasing ou financiamento misto.
Quando houver mais de uma opção, indicar a modalidade preferencial.
Impacto esperado
Apresentar indicadores quantificáveis sempre que possível.
Evitar impactos genéricos sem indicador ou evidência.
Documentos disponíveis
Listar documentos existentes e estado: disponível, em preparação, parcial ou não aplicável.
Não anunciar documentos como disponíveis se ainda não estiverem validados.
Critérios mínimos antes da publicação
A oportunidade deve 
inclui
r 
o sector, a localização, a entidade promotora, uma breve descrição, o estágio de maturidade e o contacto
 
do 
responsável.
O valor do investimento deve estar indicado ou 
mencionado como
 “em avaliação”.
O impacto esperado deve conter pelo menos uma dimensão mensurável ou uma explicação qualitativa fundamentada.
A ficha deve indicar 
a 
data de publicação, 
a 
data da última actualização e
 o
 responsável pela validação.
O resumo executivo deve ser coerente com os dados técnicos, financeiros e documentais apresentados.
O conteúdo legal, fiscal, ambiental ou cambial deve ser validado antes d
e su
a publicação como informação oficial.
Boas práticas editoriais
Usar títulos curtos e informativos para facilitar 
a 
leitura em cards e páginas de detalhe.
Começar o resumo executivo pela proposta de valor do projecto, não pela descrição institucional.
Destacar factos relevantes para decisão: localização, maturidade, investimento requerido, parceiro pretendido e impacto.
Separar claramente 
a 
informação pública d
a
 informação reservada ao investidor
,
 mediante solicitação ou acordo de confidencialidade.
Actualizar a ficha sempre que houver mudança de estado, 
de 
documentos,
 de
 contacto, 
de 
financiamento ou 
de 
entidade responsável.
Usar a mesma terminologia em todas as fichas para garantir 
a 
comparabilidade entre
 as
 oportunidades.
Checklist final:
 confirmar se a oportunidade está completa, classificada correctamente, actualizada, validada pela entidade responsável e apresentada de forma compreensível para investidores nacionais e estrangeiros
.
Anexo executivo: ciclo de vida do investidor
Finalidade do anexo:
 Este anexo apresenta uma visão executiva do ciclo de vida do investidor, estruturando as principais etapas, 
as 
responsabilidades institucionais e 
os 
resultados esperados
,
 desde a identificação de oportunidades até ao acompanhamento 
pós-investimento. O seu objectivo é servir 
de
 referência prática para a organização dos conteúdos, 
dos 
serviços e
 dos
 fluxos funcionais do Portal do Investidor.
Âmbito de aplicação:
 
O ciclo aplica-se a investidores nacionais e estrangeiros, entidades facilitadoras, instituições públicas competentes e parceiros técnicos envolvidos na preparação, submissão, validação, aprovação, implementação e monitoria de projectos de investimento em Moçambique.
Como utilizar este anexo:
 A tabela seguinte deve ser 
utilizada como matriz orientadora para definir as páginas do portal, as responsabilidades operacionais, os requisitos documentais, os pontos de contacto, os indicadores de desempenho e as funcionalidades
 futuras de acompanhamento digital.
Matriz de etapas, responsáveis e resultados
Etapa
Descrição
Responsável principal
Entidades de apoio
Resultado esperado
1. Descoberta de oportunidades
Identificação de sectores, províncias, projectos prioritários e oportunidades disponíveis no portal.
Investidor
Portal do Investidor; Tempo Lda.; entidades promotoras de projectos
Lista inicial de oportunidades compatíveis com o perfil do investidor.
2. Análise de requisitos
Consulta de requisitos legais, fiscais, sectoriais, ambientais, laborais e cambiais aplicáveis ao projecto.
Investidor
APIEX; Autoridade Tributária; Banco de Moçambique; ministérios sectoriais
Compreensão clara dos documentos, autorizações e condições necessárias.
3. Preparação do projecto
Elaboração do plano de negócios, estrutura societária, estimativa de investimento, localização e documentação de suporte.
Investidor
Consultores; instituições financeiras; parceiros locais; serviços jurídicos
Dossier de investimento tecnicamente estruturado e pronto para submissão.
4. Submissão institucional
Submissão formal do projecto ou pedido de orientação através do portal ou junto da entidade competente.
Investidor
Portal do Investidor; APIEX; entidades licenciadoras
Pedido registado, com número de acompanhamento e comprovativo de submissão.
5. Validação e pareceres
Análise documental, verificação de enquadramento legal, validação fiscal, avaliação sectorial e eventual solicitação de informação adicional.
Entidade competente
APIEX; Autoridade Tributária; Banco de Moçambique; ministérios sectoriais; autoridades locais
Parecer técnico-institucional emitido e pendências identificadas ou resolvidas.
6. Aprovação e registo
Emissão de autorização, certificado, licença, registo cambial ou reconhecimento de incentivos aplicáveis.
Entidade competente
APIEX; Banco de Moçambique; Conservatória; Autoridade Tributária
Projecto aprovado ou registado, com direitos, obrigações e condições claramente definidos.
7. Implementação do investimento
Execução do projecto, mobilização de capital, contratação, aquisição de activos, construção, operação e 
cumprimento das obrigações legais.
Investidor
Entidades licenciadoras; parceiros locais; instituições financeiras; 
autoridades provinciais
Projecto em execução, com início de actividades e cumprimento dos marcos previstos.
8. Acompanhamento pós-investimento
Monitoria de desempenho, apoio na resolução de constrangimentos, actualização de licenças, expansão e reporte de indicadores.
Investidor e entidade facilitadora
Portal do Investidor; APIEX; Tempo Lda.; entidades públicas relevantes
Investimento acompanhado, com suporte contínuo, melhoria da retenção e identificação de novas oportunidades.
Gráficos de desempenho
Nota metodológica:
 Os gráficos abaixo são apresentados como modelo executivo de acompanhamento. Os valores percentuais são indicativos e devem ser substituídos por dados reais do portal, da entidade facilitadora ou das instituições competentes quando o sistema de monitoria estiver operacional.
Etapa do ciclo
Indicador de desempenho
Meta operacional
Desempenho ilustrativo
Leitura executiva
Descoberta de oportunidades
Oportunidades publicadas e actualizadas
80%
████████░░
 80%
Bom nível de actualização da carteira, exigindo manutenção contínua por sector e província.
Análise de requisitos
Requisitos disponíveis por sector
70%
███████░░░
 70%
Cobertura satisfatória, mas ainda dependente da validação jurídica e sectorial das entidades competentes.
Preparação do projecto
Dossiers completos antes da submissão
65%
██████░░░░
 65%
Requer reforço de modelos, checklists e orientação técnica ao investidor.
Submissão institucional
Submissões registadas com número de acompanhamento
75%
███████░░░
 75%
Indica maturidade operacional moderada e necessidade de automatizar confirmações e notificações.
Validação e pareceres
Processos analisados dentro do prazo definido
55%
█████░░░░░
 55%
Principal ponto crítico do ciclo, exigindo acordos de nível de serviço e integração institucional.
Aprovação e registo
Projectos aprovados ou registados sem pendências críticas
60%
██████░░░░
 60%
Desempenho intermediário, dependente da qualidade documental e da articulação entre entidades.
Implementação do investimento
Projectos em execução conforme marcos previstos
50%
█████░░░░░
 50%
Exige mecanismos de acompanhamento de marcos físicos, financeiros e regulatórios.
Acompanhamento pós-investimento
Constrangimentos resolvidos e projectos acompanhados
45%
████░░░░░░
 45%
Área com maior necessidade de institucionalização do suporte pós-investimento e recolha de feedback.
Nota de leitura:
 As barras de desempenho permitem visualizar rapidamente os pontos fortes e os pontos críticos do ciclo. O foco inicial deve recair sobre 
a 
validação institucional, 
a 
implementação e
 o
 acompanhamento pós-investimento, por serem etapas com maior dependência de coordenação interinstitucional e maior impacto na experiência do investidor.
Análise de tendências
Síntese executiva:
 A leitura combinada dos indicadores sugere uma tendência positiva nas etapas iniciais do ciclo, especialmente na descoberta de oportunidades, 
na 
análise de requisitos e
 na
 submissão institucional. Contudo, observa-se uma tendência de desaceleração nas fases que dependem de validação interinstitucional, implementação efectiva e acompanhamento pós-investimento, 
o que indica a necessidade de reforçar os mecanismos de coordenação, os prazos de resposta e a monitoria
 contínua.
Dimensão analisada
Tendência observada
Interpretação executiva
Implicação para decisão
Etapas iniciais do ciclo
Crescente
O portal tende a gerar maior valor quando actua como vitrine, guia de orientação e ponto de entrada para oportunidades de investimento.
Priorizar a actualização da carteira de projectos, filtros sectoriais e conteúdos orientadores.
Qualidade documental
Moderadamente crescente
A preparação dos dossiers melhora quando existem modelos, checklists e requisitos claros, mas ainda depende da maturidade técnica do investidor.
Disponibilizar modelos normalizados, guias passo a passo e apoio especializado antes da submissão.
Validação institucional
Estável com risco de estrangulamento
A etapa de pareceres concentra dependências entre entidades e pode tornar-se o principal gargalo do ciclo.
Definir prazos de resposta, pontos focais, regras de escalonamento e acordos de nível de serviço.
Aprovação e registo
Intermitente
O desempenho varia conforme a qualidade documental, a clareza normativa e a articulação entre APIEX, Autoridade Tributária, Banco de Moçambique e entidades sectoriais.
Adoptar mecanismos de acompanhamento por estado do processo e alertas automáticos de pendências.
Implementação do investimento
Descendente sem monitoria activa
Após a aprovação, há risco de perda de visibilidade sobre marcos físicos, financeiros e regulatórios do projecto.
Criar painel de marcos de implementação, reporte periódico e acompanhamento de constrangimentos.
Acompanhamento pós-investimento
Crítica e subdesenvolvida
A baixa maturidade desta etapa reduz a capacidade de 
retenção, expansão e aprendizagem institucional.
Institucionalizar suporte pós-investimento, 
recolha de feedback e indicadores de satisfação do investidor.
Leitura estratégica:
 A tendência geral indica que o portal deve evoluir de uma plataforma predominantemente informativa para um sistema de facilitação e 
de 
monitor
amento
. A prioridade de curto prazo deve concentrar-se na melhoria dos conteúdos, da navegação e da preparação documental; a prioridade de médio prazo deve reforçar o acompanhamento institucional; e a prioridade de longo prazo deve consolidar a integração transaccional e o suporte pós-investimento.
Gráficos comparativos históricos
Nota metodológica:
 Os dados históricos apresentados abaixo são ilustrativos e servem como modelo de monitoria comparativa. A matriz deve ser actualizada anualmente com dados reais 
provenientes 
do portal, da entidade facilitadora e das instituições competentes.
Indicador-chave
2024
2025
2026
Variação 2024–2026
Gráfico comparativo
Leitura executiva
Oportunidades publicadas e actualizadas
45%
62%
80%
+35 p.p.
2024: 
████░░░░░░
 45%
2025: 
██████░░░░
 62%
2026: 
████████░░
 80%
Tendência crescente, indicando maior maturidade na organização da carteira de oportunidades.
Requisitos sectoriais disponíveis
38%
55%
70%
+32 p.p.
2024: 
███░░░░░░░
 38%
2025: 
█████░░░░░
 55%
2026: 
███████░░░
 70%
Melhoria consistente, embora continue dependente de validação jurídica e sectorial.
Dossiers completos na primeira submissão
30%
48%
65%
+35 p.p.
2024: 
███░░░░░░░
 30%
2025: 
█████░░░░░
 48%
2026: 
██████░░░░
 65%
Avanço relevante, sugerindo impacto positivo de checklists, modelos e apoio técnico prévio.
Processos analisados dentro do prazo
42%
49%
55%
+13 p.p.
2024: 
████░░░░░░
 42%
2025: 
█████░░░░░
 49%
2026: 
█████░░░░░
 55%
Crescimento lento, mantendo a validação institucional como principal gargalo do ciclo.
Projectos em implementação acompanhada
25%
37%
50%
+25 p.p.
2024: 
██░░░░░░░░
 25%
2025: 
████░░░░░░
 37%
2026: 
█████░░░░░
 50%
Melhoria moderada, mas ainda insuficiente para garantir monitoria plena dos marcos de execução.
Acompanhamento pós-investimento
18%
30%
45%
+27 p.p.
2024: 
██░░░░░░░░
 18%
2025: 
███░░░░░░░
 30%
2026: 
████░░░░░░
 45%
Apesar da evolução, continua a ser a área mais frágil e estratégica para retenção de investidores.
Leitura dos gráficos históricos:
 A evolução comparativa mostra ganhos expressivos nas etapas de publicação de oportunidades, disponibilização de requisitos e preparação documental. No entanto, os indicadores ligados à validação institucional, 
à implementação e ao acompanhamento pós-investimento crescem em ritmo mais lento, revelando a necessidade de acções estratégicas orientadas para a coordenação interinstitucional, a automação de processos e o suporte
 continuado ao investidor.
Recomendações estratégicas
Prioridade estratégica
Acção recomendada
Responsável principal
Horizonte temporal
Impacto esperado
Alta
Instituir pontos focais por entidade competente para reduzir atrasos na validação e emissão de pareceres.
Entidade coordenadora do portal
Curto prazo
Redução de gargalos, melhoria dos prazos de resposta e maior previsibilidade para o investidor.
Alta
Criar acordos de nível de serviço para etapas críticas, incluindo prazos, responsabilidades e regras de escalonamento.
APIEX e entidades sectoriais
Curto prazo
Maior responsabilização institucional e melhor controlo do tempo de tramitação.
Alta
Implementar painel de acompanhamento de processos com estados, pendências, alertas e histórico de interacções.
Equipa técnica do portal
Curto a médio prazo
Transparência processual, redução de incerteza e melhoria da experiência do investidor.
Média
Disponibilizar modelos normalizados de dossier de investimento, checklists e guias por sector.
Portal do Investidor; consultores técnicos
Curto prazo
Aumento da proporção de submissões completas e redução de devoluções por documentação insuficiente.
Média
Integrar indicadores de implementação física, financeira e regulatória dos projectos aprovados.
Entidade facilitadora; investidores; entidades licenciadoras
Médio prazo
Melhor acompanhamento da execução e identificação antecipada de riscos ou constrangimentos.
Média
Criar mecanismo de feedback do investidor após cada etapa relevante do ciclo.
Portal do Investidor
Médio prazo
Melhoria contínua dos serviços e identificação de falhas recorrentes no processo.
Futura
Desenvolver integração transaccional com sistemas públicos para submissão, validação e actualização automática de dados.
Governo; entidades públicas; equipa técnica
Longo prazo
Transformação do portal em plataforma de facilitação integrada e 
semi-transaccional
.
Futura
Institucionalizar unidade de apoio pós-investimento para retenção, expansão e resolução de constrangimentos.
Entidade facilitadora; APIEX; parceiros institucionais
Longo prazo
Maior retenção de investidores, aumento de reinvestimentos e fortalecimento da confiança institucional.
Plano técnico detalhado da página principal
Esta secção traduz as recomendações funcionais do documento para um plano inicial de desenvolvimento em 
React
 e 
Laravel
, com foco na construção da página principal do Portal do Investidor, na organização dos botões, no layout, nos fluxos de informação e nos endpoints necessários para alimentar a 
home
 com dados dinâmicos.
1. Objectivo funcional da página principal
A página principal deve funcionar como porta de entrada para três decisões essenciais do investidor: descobrir oportunidades, compreender requisitos e iniciar contacto ou manifestação de interesse. O layout deve reduzir fricção, apresentar informação de forma visual e conduzir o utilizador para acções claras.
2. Estrutura recomendada do layout da 
home
Secção
Função
Elementos principais
Header
Navegação principal e identidade da plataforma.
Logótipo, menu, selector PT | EN, botão de login e chamada “Submeter interesse”.
Hero 
section
Comunicar a proposta de valor do portal.
Título, subtítulo, botões principais e imagem/ilustração institucional.
Pesquisa inteligente
Permitir procura rápida por temas de investimento.
Campo de pesquisa, sugestões, filtros rápidos por sector, província e tipo de oportunidade.
Sectores estratégicos
Apresentar áreas prioritárias de investimento.
Cards de Energia, Agronegócio, Turismo, Infraestruturas, Economia Azul e Mercado Financeiro.
Oportunidades em destaque
Mostrar projectos prioritários ou recentemente actualizados.
Cards com sector, localização, maturidade, valor estimado e botões de acção.
Como investir
Explicar o percurso do investidor em etapas simples.
Fluxo com constituição de empresa, NUIT, APIEX, licenciamento, conta bancária e registo cambial.
Incentivos e legislação
Dar acesso rápido a informação legal e fiscal.
Links para benefícios fiscais, ZEE/
ZFI
, repatriamento, legislação e guias descarregáveis.
Bolsa de parcerias
Ligar investidores, empresas e promotores de projectos.
Botões para procurar parceiro, captar capital, propor projecto ou manifestar interesse.
Indicadores do portal
Demonstrar dinâmica e credibilidade.
Número de oportunidades, sectores, províncias cobertas, manifestações de interesse e projectos em análise.
Footer
Disponibilizar contactos, links legais e navegação complementar.
Contactos, redes, termos de uso, política de privacidade e links institucionais.
3. Botões principais da página inicial
Explorar oportunidades:
 encaminha para a lista de oportunidades com filtros avançados.
Como investir em Moçambique:
 abre o guia passo a passo do investidor.
Ver incentivos:
 apresenta benefícios fiscais, aduaneiros, ZEE, 
ZFI
 e requisitos de elegibilidade.
Manifestar interesse:
 abre formulário simples para recolha de dados do investidor e oportunidade pretendida.
Entrar na área do investidor:
 encaminha para autenticação e dashboard reservado.
Submeter projecto:
 permite que empresas, promotores ou entidades apresentem oportunidades para validação.
4. Componentes 
React
 para a página principal
Componente 
React
Responsabilidade
Dados consumidos
HomePage
Componente agregador da página principal.
Resumo da 
home
, sectores, oportunidades, indicadores e conteúdos institucionais.
Header
Apresenta navegação, logótipo, idiomas e botões de acção.
Menus, idiomas activos e estado de autenticação.
LanguageSwitcher
Permite alternância entre português e inglês.
Locale
 actual e rotas traduzidas.
HeroSection
Mostra proposta de valor e chamadas principais.
Título, subtítulo, imagem, botões e estatísticas resumidas.
SmartSearchBar
Permite pesquisa por palavra-chave e sugestões.
Sugestões, sectores, províncias e temas populares.
QuickActionButtons
Apresenta atalhos para acções relevantes.
Lista de acções, ícones, 
URLs
 e prioridade.
SectorGrid
Mostra os sectores estratégicos em formato de cards.
Nome, descrição, imagem, número de oportunidades e 
slug
 do sector.
SectorCard
Card individual de cada sector.
Dados do sector e chamada de acção.
FeaturedOpportunities
Lista oportunidades em destaque ou recentes.
Projectos publicados, maturidade, localização, valor e impacto.
OpportunityCard
Card resumido de cada oportunidade.
Nome, sector, província, investimento estimado, maturidade e acções.
InvestorJourney
Apresenta o fluxo “Como investir”.
Etapas, requisitos, entidades envolvidas e documentos.
StepCard
Representa cada etapa do percurso do investidor.
Título, descrição, ordem, documentos e entidades.
IncentivesPreview
Resumo de incentivos e legislação relevante.
Categorias de incentivos, ZEE/
ZFI
, documentos e links.
PartnershipExchangeBlock
Apresenta a Bolsa de Parcerias, Investidores e Capital.
Perfis, tipos de ligação e botões de submissão.
PortalStats
Mostra indicadores rápidos da plataforma.
Total de oportunidades, sectores, províncias, solicitações e projectos activos.
NewsletterOrContactCTA
Convida o utilizador a solicitar informação ou apoio.
Formulário simples, contactos e categorias de interesse.
Footer
Apresenta navegação final, contactos e documentos legais.
Links institucionais, políticas, contactos e redes sociais.
5. Fluxo de informação proposto para a 
home
O utilizador entra na página principal e identifica rapidamente a proposta de valor do portal.
Escolhe uma acção inicial: pesquisar oportunidade, ver guia, consultar incentivos ou manifestar interesse.
O sistema apresenta filtros e sugestões alinhadas com sector, província, fase de maturidade e tipo de financiamento.
O utilizador selecciona uma oportunidade ou um tema legal/regulatório.
A plataforma apresenta ficha resumida, requisitos, entidade competente, documentos disponíveis e botões de acção.
Se houver interesse, o utilizador preenche formulário simples ou cria conta para continuar na área reservada.
O 
Laravel
 regista o pedido, gera referência interna e permite acompanhamento posterior.
6. Endpoints 
Laravel
 necessários para a 
home
Método
Endpoint
Finalidade
GET
/
api
/v1/
home
Carregar a configuração geral da página principal: 
hero
, botões, blocos activos, destaques e textos institucionais.
GET
/
api
/v1/
sectors
Listar sectores estratégicos apresentados na 
home
.
GET
/
api
/v1/
sectors
/{
slug
}
Obter detalhes de um sector específico e preparar ligação para página sectorial.
GET
/
api
/v1/
provinces
Listar províncias utilizadas nos filtros de pesquisa e oportunidades.
GET
/
api
/v1/
opportunities
/
featured
Listar oportunidades em destaque para os cards da página principal.
GET
/
api
/v1/
opportunities
/
latest
Listar oportunidades recentemente publicadas ou actualizadas.
GET
/
api
/v1/
opportunities
/
search
Pesquisar oportunidades por palavra-chave, sector, província, maturidade, entidade promotora e modalidade de financiamento.
GET
/
api
/v1/
investor-journey
Carregar etapas do fluxo “Como investir em Moçambique”.
GET
/
api
/v1/incentives/
preview
Carregar resumo de incentivos fiscais, aduaneiros, ZEE/
ZFI
 e benefícios por sector.
GET
/
api
/v1/
legal-resources
/featured
Listar documentos legais e guias principais destacados na 
home
.
GET
/
api
/v1/partnership-exchange/summary
Carregar resumo da Bolsa de Parcerias, Investidores e Capital.
POST
/
api
/v1/
interest-requests
Receber manifestação de interesse de investidor, empresa ou promotor de projecto.
POST
/
api
/v1/project-submissions/pre-check
Receber pedido preliminar de submissão de projecto antes da criação de conta ou processo formal.
POST
/
api
/v1/
contact-requests
Receber pedidos de contacto, esclarecimento ou agendamento de reunião.
GET
/
api
/v1/portal-
stats
Carregar indicadores públicos da plataforma: oportunidades, sectores, províncias, pedidos e projectos em análise.
GET
/
api
/v1/
search
/
suggestions
Gerar sugestões para a barra de pesquisa com termos como APIEX, benefícios fiscais, ZEE, NUIT e repatriamento.
GET
/
api
/v1/
settings
/
public
Carregar definições públicas: idiomas activos, contactos, links institucionais, redes sociais e configurações da 
home
.
9. Estrutura de pastas recomendada para 
React
 e 
Laravel
A estrutura deve separar claramente a camada de apresentação em 
React
, a camada de API em 
Laravel
 e os domínios funcionais do portal: oportunidades, sectores, recursos legais, manifestações de interesse, estatísticas públicas e autenticação. Esta organização facilita manutenção, escalabilidade, testes e futura integração com entidades públicas.
Camada
Estrutura de pastas
Finalidade
React
src
/components, 
src
/pages, 
src
/routes, 
src
/services, 
src
/hooks, 
src
/store, 
src
/assets, 
src
/i18n, 
src
/utils
Organizar componentes reutilizáveis, páginas, chamadas à API, gestão de estado, internacionalização e utilitários.
Laravel
app/Http/Controllers/Api/V1, app/Models, app/Services, app/Repositories, app/Http/Requests, app/Policies, database/migrations, routes/
api.php
Separar 
controllers
, regras de negócio, validação, autorização, persistência e rotas da API.
Domínios
Home, Sectors, Opportunities, Provinces, Incentives, 
LegalResources
, 
InterestRequests
, 
PortalStats
, Auth
Agrupar funcionalidades por área do portal, evitando 
controllers
 genéricos e lógica dispersa.
10. Exemplo de 
payload
 
JSON
 para o 
endpoint
 /
api
/v1/
home
O 
endpoint
 agregado da 
home
 deve devolver apenas a informação pública necessária ao primeiro carregamento da página. Dados sensíveis, documentos reservados, contactos privados e informação processual devem permanecer fora deste 
payload
 e ser disponibilizados apenas em endpoints autenticados.
Exemplo simplificado: { "
locale
": "
pt
", "
hero
": { "
title
": "Invista em Moçambique com informação clara e oportunidades estruturadas", "
subtitle
": "Explore sectores estratégicos, incentivos, requisitos e oportunidades de investimento validadas.", "
primaryButton
": { "
label
": "Explorar oportunidades", "
url
": "/oportunidades" }, "
secondaryButton
": { "
label
": "Como investir", "
url
": "/guia-do-investidor" } }, "
quickActions
": [ { "
label
": "Ver incentivos", "
url
": "/incentivos" }, { "
label
": "Manifestar interesse", "
url
": "/manifestar-interesse" }, { "
label
": "Área do investidor", "
url
": "/login" } ], "
sectors
": [ { "
name
": "Agronegócio", "
slug
": "
agronegocio
", "
description
": "Oportunidades em produção, processamento e cadeias de valor agrícolas.", "
opportunitiesCount
": 12 } ], "
featuredOpportunities
": [ { "
reference
": "INV-MZ-AGR-001", "
title
": "Unidade de processamento agrícola", "sector": "Agronegócio", "
province
": "Nampula", "
maturity
": "Projecto estruturado", "
estimatedInvestment
": { "
amount
": 2500000, "
currency
": "USD" }, "
actions
": ["Ver ficha", "Manifestar interesse"] } ], "
stats
": { "
opportunities
": 48, "
sectors
": 6, "
provinces
": 11, "
interestRequests
": 120 }, "
legalHighlights
": [ { "
title
": "Guia de incentivos fiscais", "
category
": "Fiscalidade", "
url
": "/recursos/guia-incentivos" } ] }
11. Validação e segurança dos endpoints
Endpoint
Validação principal
Segurança recomendada
GET
 /
api
/v1/
home
Validar 
locale
, estado de publicação e limite de itens devolvidos.
Cache público, limitação de campos expostos, compressão, 
headers
 de segurança e 
CORS
 limitado ao domínio autorizado.
GET /
api
/v1/opportunities/search
Validar 
query
, sector, província, maturidade, modalidade de financiamento, página e limite máximo por pedido.
Rate 
limiting
, sanitização de entrada, paginação obrigatória, protecção contra enumeração excessiva e 
logs
 de pesquisa.
POST /
api
/v1/interest-requests
Validar nome, email, telefone, tipo de investidor, sector de interesse, mensagem, consentimento 
de privacidade e oportunidade associada.
CSRF
 quando aplicável, 
captcha
 ou mecanismo anti-spam, rate 
limiting
 por IP/email, validação de 
domínio, encriptação de dados sensíveis e auditoria.
POST /
api
/v1/project-submissions/pre-check
Validar entidade promotora, sector, localização, descrição, valor estimado, estado documental e anexos permitidos.
Autenticação recomendada, verificação de ficheiros, limitação de tamanho, antivírus, armazenamento seguro e registo de autoria.
GET /
api
/v1/
legal-resources
/featured
Validar idioma, categoria, estado de publicação e data de actualização.
Publicar apenas documentos validados, usar 
URLs
 assinadas quando necessário e manter histórico de versões.
12. Código inicial 
React
 para 
HomePage
O exemplo abaixo apresenta uma base simples para a página principal em 
React
, consumindo o 
endpoint
 agregado 
/
api
/v1/
home
 e distribuindo os dados pelos componentes principais da 
home
.
Exemplo: criar o ficheiro 
src
/
pages
/
HomePage.jsx
 com uma estrutura baseada em carregamento inicial, estado de 
loading
, tratamento de erro e 
renderização
 dos componentes 
Header
, 
HeroSection
, 
SectorGrid
, 
FeaturedOpportunities
, 
InvestorJourney
, 
IncentivesPreview
, 
PortalStats
 e 
Footer
.
Modelo de implementação: importar 
useEffect
 e 
useState
; chamar 
getHomeData
 a partir de 
src
/
services
/homeService.js; guardar a resposta em estado local; apresentar mensagem de carregamento enquanto a API responde; apresentar mensagem de erro se a API falhar; e 
renderizar
 cada bloco apenas quando os dados estiverem disponíveis.
Exemplo lógico de 
HomePage.jsx
:
 
const
 [
home
, 
setHome
] = 
useState
(
null
); 
useEffect
(
() => 
{ 
getHomeData
(
).
then
(
setHome
).
catch
(() => 
setError
(
true
))
; }
, []); se 
loading
 mostrar “A carregar...”; se erro mostrar “Não foi possível carregar os dados”; se existir 
home
, 
renderizar
 
HeroSection
 com 
home.hero
, 
SectorGrid
 com 
home.sectors
, 
FeaturedOpportunities
 com 
home.featuredOpportunities
 e 
PortalStats
 com 
home.stats
.
13. Serviço 
React
 para consumo da API da 
home
Criar o ficheiro 
src
/
services
/homeService.js
 para centralizar a chamada à API. O serviço deve usar a variável de ambiente 
VITE_API_URL
 e devolver a resposta 
JSON
 do 
endpoint
 público da 
home
.
Exemplo lógico:
 definir 
API_URL
 = 
import.meta.env.VITE
_API_URL
; exportar a função 
getHomeData
; fazer 
fetch
 para `${
API_
URL
}/
api
/v1/
home?locale
=
pt
`; validar 
response.ok
; lançar erro quando a resposta não for bem-sucedida; e devolver 
response.json
().
14. Rotas API 
Laravel
 para a 
home
As rotas públicas da 
home
 devem ser registadas em 
routes
/
api.php
, agrupadas por versão e com 
middleware
 de limitação de pedidos. Nesta fase inicial, a maioria dos endpoints pode ser pública, excepto submissões mais sensíveis ou endpoints administrativos.
Rota 
Laravel
Controller
Função
Route::get(
'/home', [
HomeController
::
class, 'show'])
HomeController
Devolver 
payload
 agregado da página principal.
Route::get(
'/sectors', [
SectorController
::
class, 'index'])
SectorController
Listar sectores estratégicos publicados.
Route::get(
'/opportunities/featured', [
OpportunityController
::
class, 'featured'])
OpportunityController
Listar oportunidades em destaque.
Route::get(
'/opportunities/search', [
OpportunityController
::
class, 'search'])
OpportunityController
Pesquisar oportunidades por filtros públicos.
Route::get(
'/portal-stats', [
PortalStatsController
::
class, 'index'])
PortalStatsController
Devolver indicadores públicos da plataforma.
Route::post(
'/interest-requests', [
InterestRequestController
::
class, 'store'])
InterestRequestController
Registar manifestação de interesse.
Route::post(
'/contact-requests', [
ContactRequestController
::
class, 'store'])
ContactRequestController
Registar pedido de contacto.
15. Exemplo de 
Controller
 
Laravel
 para a 
home
O 
HomeController
 deve evitar lógica pesada dentro do 
controller
. A recomendação é delegar a composição do 
payload
 para um serviço, por exemplo 
HomeService
, que consulta sectores, oportunidades em destaque, estatísticas e blocos de conteúdo publicados.
21. homeService.js com lógica de API
22. 
StoreInterestRequestRequest.php
HomePage
 integrada, testes 
Postman
 e melhorias UX
1. 
HomePage.jsx
 completo integrado
Ficheiro
: 
src
/pages/
HomePage.jsx
import { 
useEffect
, 
useState
 } from 'react';
import { 
getHomeData
 } from '../services/
homeService
';
import 
FeaturedOpportunities
 from '../components/opportunities/
FeaturedOpportunities
';
export default function 
HomePage
() {
  const [home, 
setHome
] = 
useState
(null);
  const [locale, 
setLocale
] = 
useState
('pt');
  const [loading, 
setLoading
] = 
useState
(true);
  const [error, 
setError
] = 
useState
(null);
  
useEffect
(() => {
    async function 
loadHome
() {
      try {
        
setLoading
(true);
        
setError
(null);
        const response = await 
getHomeData
(locale);
        
setHome
(
response.data
 || response);
      } catch (err) {
        
setError
(
err.message
 || '
Não
 
foi
 
possível
 
carregar
 a 
página
 principal.');
      } finally {
        
setLoading
(false);
      }
    }
    
loadHome
();
  }, [locale]);
  if (loading) {
    return <main 
className
="home-page loading-state"><p>A 
carregar
 dados do Portal do 
Investidor
...</p></main>;
  }
  if (error) {
    return (
      <main 
className
="home-page error-state">
        <h1>Portal do 
Investidor
</h1>
        <p>{error}</p>
        <button 
onClick
={() => 
window.location.reload
()}>
Tentar
 
novamente
</button>
      </main>
    );
  }
  const hero = 
home?.hero
 || {};
  const 
quickActions
 = home?.
quickActions
 || [];
  const sectors = 
home?.sectors
 || [];
  const stats = 
home?.stats
 || {};
  const 
investorJourney
 = home?.
investorJourney
 || [];
  const 
legalHighlights
 = home?.
legalHighlights
 || [];
  const 
featuredOpportunities
 = home?.
featuredOpportunities
 || [];
  return (
    <main 
className
="home-page">
      <header 
className
="site-header">
        <a 
href
="/" 
className
="brand">
INVESTE
 Moçambique</a>
        <nav 
className
="main-nav" aria-label="
Navegação
 principal">
          <a 
href
="/
oportunidades
">
Oportunidades
</a>
          <a 
href
="/
guia
-do-
investidor
">Como 
investir
</a>
          <a 
href
="/
incentivos
">
Incentivos
</a>
          <a 
href
="/
recursos
">
Recursos
 
legais
</a>
        </nav>
        <div 
className
="header-actions">
          <select value={locale} 
onChange
={(e) => 
setLocale
(
e.target.value
)} aria-label="
Seleccionar
 
idioma
">
            <option value="pt">PT</option>
            <option value="
en
">EN</option>
          </select>
          <a 
href
="/login" 
className
="
btn
 secondary">
Área
 do 
investidor
</a>
        </div>
      </header>
      <section 
className
="hero-section">
        <div 
className
="hero-content">
          <span 
className
="hero-kicker">Portal do 
Investidor
</span>
          <h1>{
hero.title
 || '
Invista
 
em
 Moçambique com 
informação
 
clara
 e 
oportunidades
 
estruturadas
'}</h1>
          <p>{
hero.subtitle
 || 'Explore 
sectores
 
estratégicos
, 
incentivos
, 
requisitos
 e 
oportunidades
 de 
investimento
.'}</p>
          <div 
className
="hero-actions">
            <a 
href
={hero.
primaryButton
?.
url
 || '/
oportunidades
'} 
className
="
btn
 primary">{hero.
primaryButton
?.label || '
Explorar
 
oportunidades
'}</a>
            <a 
href
={hero.
secondaryButton
?.
url
 || '/
guia
-do-
investidor
'} 
className
="
btn
 secondary">{hero.
secondaryButton
?.label || 'Como 
investir
'}</a>
          </div>
        </div>
      </section>
      <section 
className
="quick-actions">
        {
quickActions.map
((action) => (
          <a key={
action.label
} 
href
={action.url} 
className
="quick-action-card">
            <strong>{
action.label
}</strong>
            <span>{
action.description
 || '
Aceder
'}</span>
          </a>
        ))}
      </section>
      <section 
className
="sector-section">
        <div 
className
="section-header">
          <h2>
Sectores
 
estratégicos
</h2>
          <p>
Encontre
 
oportunidades
 
por
 
área
 
económica
 
prioritária
.</p>
        </div>
        <div 
className
="sector-grid">
          {
sectors.map
((sector) => (
            <a key={sector.id || 
sector.slug
} 
href
={`/
sectores
/${
sector.slug
}`} 
className
="sector-card">
              <h3>{sector.name}</h3>
              <p>{
sector.description
}</p>
              <span>{
sector.opportunitiesCount
 || 0} 
oportunidades
</span>
            </a>
          ))}
        </div>
      </section>
      <
FeaturedOpportunities
 
initialItems
={
featuredOpportunities
} />
      <section 
className
="investor-journey">
        <div 
className
="section-header">
          <h2>Como 
investir
 
em
 Moçambique</h2>
          <p>Siga o 
percurso
 
básico
 do 
investidor
, 
desde
 a 
preparação
 
até
 
ao
 
acompanhamento
.</p>
        </div>
        <div 
className
="journey-grid">
          {
investorJourney.map
((step, index) => (
            <article key={step.id || index} 
className
="journey-step">
              <span>{index + 1}</span>
              <h3>{
step.title
}</h3>
              <p>{
step.description
}</p>
            </article>
          ))}
        </div>
      </section>
      <section 
className
="legal-highlights">
        <div 
className
="section-header">
          <h2>
Incentivos
 e 
recursos
 
legais
</h2>
          <p>
Aceda
 a 
guias
, 
normas
 e 
documentos
 
essenciais
 para 
preparar
 o 
investimento
.</p>
        </div>
        <div 
className
="legal-grid">
          {
legalHighlights.map
((item) => (
            <a key={
item.title
} 
href
={item.url} 
className
="legal-card">
              <span>{
item.category
 || '
Recurso
'}</span>
              <h3>{
item.title
}</h3>
            </a>
          ))}
        </div>
      </section>
      <section 
className
="portal-stats">
        <div 
className
="stat-card"><strong>{
stats.opportunities
 || 0}</strong><span>
Oportunidades
</span></div>
        <div 
className
="stat-card"><strong>{
stats.sectors
 || 0}</strong><span>
Sectores
</span></div>
        <div 
className
="stat-card"><strong>{
stats.provinces
 || 0}</strong><span>
Províncias
</span></div>
        <div 
className
="stat-card"><strong>{
stats.interestRequests
 || 0}</strong><span>
Manifestações
 de interesse</span></div>
      </section>
      <section 
className
="home-
cta
">
        <h2>Pronto para 
avançar
?</h2>
        <p>
Manifeste
 interesse, 
solicite
 
orientação
 
ou
 
submeta
 
uma
 
oportunidade
 para 
avaliação
 
inicial
.</p>
        <div 
className
="
cta
-actions">
          <a 
href
="/
manifestar
-interesse" 
className
="
btn
 primary">
Manifestar
 interesse</a>
          <a 
href
="/
contacto
" 
className
="
btn
 secondary">
Solicitar
 
contacto
</a>
        </div>
      </section>
    </main>
  );
}
2. Exemplo de teste com 
Postman
No 
Postman
, criar uma 
collection
 chamada Portal do Investidor API. Definir a variável 
base_url
 com o valor http://localhost:8000. Testar primeiro os endpoints públicos 
GET
 e depois o 
endpoint
 POST de manifestação de interesse.
Teste
Método
URL
Objectivo
Carregar 
home
GET
{{
base_
url
}}/
api
/v1/
home?locale
=pt
Confirmar
 se o payload 
agregado
 devolve hero, 
quickActions
, sectors, 
featuredOpportunities
, 
investorJourn
ey
, 
legalHighlights
 e stats.
Oportunidades em destaque
GET
{{
base_
url
}}/
api
/v1/opportunities/
featured?limit
=6
Confirmar se a API devolve oportunidades publicadas e marcadas como destaque.
Pesquisa por sector
GET
{{base_url
}}/
api/v1/opportunities/search?sector=agronegocio&province=Nampula&per_page=12
Validar filtros por sector, província e paginação.
Pesquisa por palavra-chave
GET
{{base_
url}}/
api/v1/opportunities/search?q=solar&maturity_level=3
Confirmar busca textual combinada com maturidade.
Manifestação de interesse
POST
{{
base_
url
}}/
api
/v1/interest-requests
Validar criação de pedido de interesse e mensagens de erro quando faltarem campos obrigatórios.
Exemplo de body 
JSON
 para POST /
api
/v1/
interest-requests
:
{
  "
opportunity_id
": 1,
  "
investor_name
": "João Investidor",
  "
investor_type
": "
company
",
  "
company_name
": "
Moz
 Capital 
Partners
",
  "email": "joao@example.com",
  "
phone
": "+258 84 000 0000",
  "country": "Moçambique",
  "
sector_interest
": "Agronegócio",
  "
investment_capacity
": "500k_1m",
  "
message
": "Pretendo receber mais informação sobre a oportunidade e avaliar possibilidade de parceria.",
  "
privacy_consent
": 
true
}
3. Melhorias específicas de UX para a 
home
Hero orientado à acção:
 usar uma frase curta de valor e dois botões dominantes: Explorar oportunidades e Como investir.
Pesquisa com sugestões reais:
 substituir 
placeholders
 genéricos por termos como benefícios fiscais, APIEX, ZEE, agricultura, energia solar e repatriamento de dividendos.
Filtros visíveis sem excesso:
 mostrar inicialmente sector, província e maturidade; deixar filtros avançados para a página de oportunidades.
Cards comparáveis:
 todos os cards de oportunidade devem mostrar sector, província, maturidade, investimento estimado e modalidade de financiamento na mesma ordem.
CTA persistente:
 manter um botão Manifestar interesse visível nos cards e no final da página.
Confiança institucional:
 criar bloco com entidades, notas de validação e data de última actualização dos conteúdos.
Bilinguidade
 imediata:
 colocar selector PT | EN no 
header
, com tradução completa da 
home
 e dos formulários.
Estados claros:
 apresentar 
skeleton
 
loading
, mensagens de erro úteis, estado de lista vazia e feedback visual após submissões.
Mobile-
first
:
 garantir cards empilhados, botões largos e formulários simples em ecrãs pequenos.
Acessibilidade:
 usar contraste adequado, labels nos formulários, navegação por teclado e textos alternativos para ícones e imagens.
Ficheiro: app/
Http
/
Requests
/
StoreInterestRequestRequest.php
<?
php
namespace
 App\
Http
\
Requests
;
use 
Illuminate
\Foundation\
Http
\
FormRequest
;
class
 
StoreInterestRequestRequest
 
extends
 
FormRequest
{
    
public
 
function
 
authorize
(): 
bool
    {
        
return
 
true
;
    }
    
public
 
function
 rules(): 
array
    {
        
return
 [
            '
opportunity_id
' => ['
nullable
', '
integer
', '
exists:opportunities,id
'],
            '
investor_name
' => ['
required
', '
string
', 'min:2', 'max:150'],
            '
investor_type
' => ['
required
', '
string
', '
in:individual,company,fund,institution
'],
            '
company_name
' => ['
nullable
', '
string
', 'max:180'],
            'email' => ['
required
', '
email:rfc,dns
', 'max:180'],
            '
phone
' => ['
nullable
', '
string
', 'max:40'],
            'country' => ['
nullable
', '
string
', 'max:100'],
            '
sector_interest
' => ['
required
', '
string
', 'max:120'],
            '
investment_capacity
' => ['
nullable
', '
string
', 'in:below_100k,100k_500k,500k_1m,above_1m'],
            '
message
' => ['
required
', '
string
', 'min:10', 'max:3000'],
            '
privacy_consent
' => ['
accepted
'],
        ];
    }
    
public
 
function
 
messages
(): 
array
    {
        
return
 [
            '
opportunity_id.integer
' => 'A oportunidade seleccionada é inválida.',
            '
opportunity_id.exists
' => 'A oportunidade seleccionada não existe.',
            '
investor_name.required
' => 'Informe o nome do investidor.',
            '
investor_name.min
' => 'O nome do investidor deve ter pelo menos :min caracteres.',
            '
investor_name.max
' => 'O nome do investidor não pode exceder :
max
 caracteres.',
            '
investor_type.required
' => 'Seleccione o tipo de investidor.',
            'investor_type.in' => 'O tipo de investidor seleccionado é inválido.',
            '
company_name.max
' => 'O nome da empresa ou instituição não pode exceder :
max
 caracteres.',
            '
email.required
' => 'Informe o email de contacto.',
            '
email.email
' => 'Informe um endereço de email válido.',
            '
email.max
' => 'O email não pode exceder :
max
 caracteres.',
            '
phone.max
' => 'O telefone não pode exceder :
max
 caracteres.',
            '
country.max
' => 'O país não pode exceder :
max
 caracteres.',
            '
sector_interest.required
' => 'Indique o sector de interesse.',
            '
sector_interest.max
' => 'O sector de interesse não pode exceder :
max
 caracteres.',
            'investment_capacity.in' => 'A capacidade indicativa de investimento seleccionada é inválida.',
            '
message.required
' => 'Descreva brevemente o interesse.',
            '
message.min
' => 'A mensagem deve ter pelo menos :min caracteres.',
            '
message.max
' => 'A mensagem não pode exceder :
max
 caracteres.',
            '
privacy_consent.accepted
' => 'É necessário aceitar a política de privacidade para submeter o pedido.',
        ];
    }
    
protected
 
function
 
prepareForValidation
(): 
void
    {
        $
this
->
merge
([
            '
investor_name
' => 
is_string
($
this
->
investor_name
) ? 
trim
($
this
->
investor_name
) : $
this
->
investor_name
,
            '
company_name
' => 
is_string
($
this
->
company_name
) ? 
trim
($
this
->
company_name
) : $
this
->
company_name
,
            'email' => 
is_string
($
this
->email) ? 
strtolower
(
trim
($
this
->email)) : $
this
->email,
            '
phone
' => 
is_string
($
this
->
phone
) ? 
trim
($
this
->
phone
) : $
this
->
phone
,
            'country' => 
is_string
($
this
->country) ? 
trim
($
this
->country) : $
this
->country,
            '
sector_interest
' => 
is_string
($
this
->
sector_interest
) ? 
trim
($
this
->
sector_interest
) : $
this
->
sector_interest
,
            '
message
' => 
is_string
($
this
->
message
) ? 
trim
($
this
->
message
) : $
this
->
message
,
        ]);
    }
    
public
 
function
 
attributes
(): 
array
    {
        
return
 [
            '
opportunity_id
' => 'oportunidade',
            '
investor_name
' => 'nome do investidor',
            '
investor_type
' => 'tipo de investidor',
            '
company_name
' => 'empresa ou instituição',
            'email' => 'email',
            '
phone
' => 'telefone',
            'country' => 'país',
            '
sector_interest
' => 'sector de interesse',
            '
investment_capacity
' => 'capacidade indicativa de investimento',
            '
message
' => 'mensagem',
            '
privacy_consent
' => 'consentimento de privacidade',
        ];
    }
}
Componentes e serviços 
React
 para oportunidades em destaque
1. 
FeaturedOpportunities.jsx
 completo
Ficheiro
: 
src
/components/opportunities/
FeaturedOpportunities.jsx
import { 
useEffect
, 
useState
 } from 'react';
import { 
getFeaturedOpportunities
 } from '../../services/
opportunityService
';
export default function 
FeaturedOpportunities
({ 
initialItems
 = null, limit = 6 }) {
  const [items, 
setItems
] = 
useState
(
initialItems
 || []);
  const [loading, 
setLoading
] = 
useState
(!
initialItems
);
  const [error, 
setError
] = 
useState
(null);
  
useEffect
(() => {
    if (
initialItems
) return;
    async function 
loadFeatured
() {
      try {
        
setLoading
(true);
        const response = await 
getFeaturedOpportunities
({ limit });
        
setItems
(
response.data
 || response || []);
      } catch (err) {
        
setError
(
err.message
 || '
Não
 
foi
 
possível
 
carregar
 as 
oportunidades
 
em
 
destaque
.');
      } finally {
        
setLoading
(false);
      }
    }
    
loadFeatured
();
  }, [
initialItems
, limit]);
  const 
formatMoney
 = (amount, currency = 'USD') => {
    if (!amount) return 'Valor 
em
 
avaliação
';
    return new 
Intl.NumberFormat
('pt-
MZ
', { style: 'currency', currency, 
maximumFractionDigits
: 0 }).format(amount);
  };
  if (loading) {
    return <section 
className
="featured-opportunities"><p>A 
carregar
 
oportunidades
 
em
 
destaque
...</p></section>;
  }
  if (error) {
    return <section 
className
="featured-opportunities"><p 
className
="error-message">{error}</p></section>;
  }
  if (!
items.length
) {
    return <section 
className
="featured-opportunities"><p>
Ainda
 
não
 
existem
 
oportunidades
 
em
 
destaque
 
publicadas
.</p></section>;
  }
  return (
    <section 
className
="featured-opportunities">
      <div 
className
="section-header">
        <span 
className
="section-kicker">
Carteira
 de 
oportunidades
</span>
        <h2>
Oportunidades
 
em
 
destaque
</h2>
        <p>Explore 
projectos
 
estruturados
 
por
 sector, 
localização
, 
maturidade
 e 
modalidade
 de 
financiamento
.</p>
      </div>
      <div 
className
="opportunity-grid">
        {
items.map
((item) => (
          <article 
className
="opportunity-card" key={item.id || 
item.slug
 || 
item.reference_code
}>
            <div 
className
="card-meta">
              <span>{item.sector?.name || 
item.sector
 || 'Sector 
não
 
definido
'}</span>
              <span>{
item.province
 || '
Localização
 
em
 
validação
'}</span>
            </div>
            <h3>{
item.title
}</h3>
            <p>{
item.summary
 || '
Oportunidade
 de 
investimento
 
em
 
validação
 editorial.'}</p>
            <ul 
className
="opportunity-details">
              <li><strong>
Referência
:</strong> {
item.reference_code
 || 'Em 
validação
'}</li>
              <li><strong>
Maturidade
:</strong> {
item.maturity_label
 || 'Em 
validação
'}</li>
              <li><strong>Investimento:</strong> {
formatMoney
(
item.investment_amount
, 
item.currency
 || 'USD')}</li>
              <li><strong>
Financiamento
:</strong> {
item.financing_modality
 || 'A 
definir
'}</li>
            </ul>
            <div 
className
="card-actions">
              <a 
href
={`/
oportunidades
/${
item.slug
 || item.id}`} 
className
="
btn
 primary">Ver 
ficha
</a>
              <a 
href
={`/
manifestar-interesse?opportunity
=${item.id}`} 
className
="
btn
 secondary">
Manifestar
 interesse</a>
            </div>
          </article>
        ))}
      </div>
    </section>
  );
}
2. opportunityService.js completo
Ficheiro
: 
src
/services/opportunityService.js
const 
API_URL
 = 
import.meta.env.VITE_API_URL
 || 'http://localhost:8000';
async function 
handleResponse
(response) {
  const data = await 
response.json
().catch(() => ({}));
  if (!
response.ok
) {
    const error = new Error(
data.message
 || 'Erro 
ao
 
comunicar
 com a API.');
    
error.status
 = 
response.status
;
    
error.errors
 = 
data.errors
 || null;
    throw error;
  }
  return data;
}
function 
buildQuery
(params = {}) {
  const query = new 
URLSearchParams
();
  
Object.entries
(params).
forEach
(([key, value]) => {
    if (value !== undefined && value !== null && value !== '') {
      
query.append
(key, value);
    }
  });
  return 
query.toString
();
}
export async function 
getFeaturedOpportunities
(params = {}) {
  const query = 
buildQuery
(params);
  const response = await fetch(`${
API_URL
}/
api
/v1/opportunities/featured${query ? 
`?$
{query}
` :
 '
'}`
, {
    headers: 
{ Accept
: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
export async function 
searchOpportunities
(
filters = {}) {
  const query = 
buildQuery
(filters);
  const response = await fetch(`${
API_URL
}/
api
/v1/opportunities/search${
query ?
 `?${query}` : ''}`, {
    headers: { Accept: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
export async function 
getOpportunityById
(id) {
  const response = await fetch(`${
API_URL
}/
api
/v1/opportunities/${id}`, {
    headers: { Accept: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
export async function 
getLatestOpportunities
(params = {}) {
  const query = 
buildQuery
(params);
  const response = await fetch(`${
API_URL
}/
api
/v1/opportunities/latest${query ? 
`?$
{query}
` :
 '
'}`
, {
    headers: 
{ Accept
: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
3. homeService.js 
completo
Ficheiro
: 
src
/services/homeService.js
const 
API_URL
 = 
import.meta.env.VITE_API_URL
 || 'http://localhost:8000';
async function 
handleResponse
(response) {
  const data = await 
response.json
().catch(() => ({}));
  if (!
response.ok
) {
    const error = new Error(
data.message
 || '
Não
 
foi
 
possível
 
carregar
 
os
 dados da 
página
 principal.');
    
error.status
 = 
response.status
;
    
error.errors
 = 
data.errors
 || null;
    throw error;
  }
  return data;
}
export async function 
getHomeData
(locale = 'pt') {
  const response = await fetch(`${
API_URL
}/
api
/v1/
home?locale
=${locale}`, {
    headers: { Accept: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
export async function 
getPortalStats
() {
  const response = await fetch(`${
API_URL
}/
api
/v1/portal-stats`, {
    headers: { Accept: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
export async function 
getSearchSuggestions
(locale = 'pt') {
  const response = await fetch(`${
API_URL
}/
api
/v1/search/
suggestions?locale
=${locale}`, {
    headers: { Accept: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
export async function 
getPublicSettings
(locale = 'pt') {
  const response = await fetch(`${
API_URL
}/
api
/v1/settings/
public?locale
=${locale}`, {
    headers: { Accept: 'application/
json
' },
  });
  return 
handleResponse
(response);
}
Formulário 
React
 para manifestação de interesse
1. 
ManifestarInteresseForm.jsx
 
completo
Ficheiro
: 
src
/components/forms/
ManifestarInteresseForm.jsx
import { 
useState
 } fr
om 'react';
import { 
createInterestRequest
 } from '../../services/
interestRequestService
';
const 
initialForm
 = {
  
opportunity_id
: '',
  
investor_name
: '',
  
investor_type
: '',
  
company_name
: '',
  email: '',
  phone: '',
  country: '',
  
sector_interest
: '',
  
investment_capacity
: '',
  message: '',
  
privacy_consent
: false,
};
export default function 
ManifestarInteresseForm
({ opportunity = null, sectors = [] }) {
  const [form, 
setForm
] = 
useState
({
    ...
initialForm
,
    
opportunity_id
: opportunity?.id || '',
    
sector_interest
: opportunity?.sector?.name || '',
  });
  const [loading, 
setLoading
] = 
useState
(false);
  const [success, 
setSuccess
] = 
useState
(null);
  const [error, 
setError
] = 
useState
(null);
  const [
validationErrors
, 
setValidationErrors
] = 
useState
({});
  const 
handleChange
 = (event) => {
    const { name, value, type, checked } = 
event.target
;
    
setForm
((
prev
) => ({ ...
prev
, [name]: type === 'checkbox' ? checked : value }));
  };
  const 
validateClientSide
 = () => {
    const errors = {};
    if (!
form.investor_name.trim
()) 
errors.investor_name
 = 'Informe o 
nome
 do 
investidor
.';
    if (!
form.investor_type
) 
errors.investor_type
 = '
Seleccione
 o 
tipo
 de 
investidor
.';
    if (!
form.email.trim
()) 
errors.email
 = 'Informe o email.';
    if (!
form.sector_interest.trim
()) 
errors.sector_interest
 = '
Indique
 o sector de interesse.';
    if (!
form.message.trim
()) 
errors.message
 = '
Descreva
 
brevemente
 o interesse.';
    if (!
form.privacy_consent
) 
errors.privacy_consent
 = 'É 
necessário
 
aceitar
 a 
política
 de 
privacidade
.';
    return errors;
  };
  const 
handleSubmit
 = async (event) => {
    
event.preventDefault
();
    
setError
(null);
    
setSuccess
(null);
    
setValidationErrors
({});
    const 
clientErrors
 = 
validateClientSide
();
    if (
Object.keys
(
clientErrors
).length > 0) {
      
setValidationErrors
(
clientErrors
);
      return;
    }
    try {
      
setLoading
(true);
      const response = await 
createInterestRequest
(form);
      
setSuccess
(
response?.message
 || '
Manifestação
 de interesse 
enviada
 com 
sucesso
.');
      
setForm
({ ...
initialForm
, 
opportunity_id
: opportunity?.id || '', 
sector_interest
: opportunity?.sector?.name || '' });
    } catch (err) {
      if (
err?.errors
) {
        
setValidationErrors
(
err.errors
);
      }
      
setError
(
err?.message
 || '
Não
 
foi
 
possível
 
enviar
 a 
manifestação
 de interesse.');
    } finally {
      
setLoading
(false);
    }
  };
  return (
    <form 
onSubmit
={
handleSubmit
} 
className
="interest-form">
      <h2>
Manifestar
 interesse</h2>
      <p>
Preencha
 
os
 dados 
abaixo
 para 
iniciar
 
contacto
 
sobre
 
esta
 
oportunidade
 de 
investimento
.</p>
      {opportunity && (
        <div 
className
="form-opportunity-box">
          <strong>
Oportunidade
 
seleccionada
:</strong>
          <p>{
opportunity.title
}</p>
        </div>
      )}
      {success && <div 
className
="alert success">{success}</div>}
      {error && <div 
className
="alert error">{error}</div>}
      <label>Nome do 
investidor
 *</label>
      <input name="
investor_name
" value={
form.investor_name
} 
onChange
={
handleChange
} placeholder="Nome 
completo
" />
      {
validationErrors.investor_name
 && <small>{
validationErrors.investor_name
}</small>}
      <label>Tipo de 
investidor
 *</label>
      <select name="
investor_type
" value={
form.investor_type
} 
onChange
={
handleChange
}>
        <option value="">
Seleccione
</option>
        <option value="individual">
Investidor
 individual</option>
        <option value="company">Empresa</option>
        <option value="fund">Fundo de 
investimento
</option>
        <option value="institution">
Instituição
</option>
      </select>
      {
validationErrors.investor_type
 && <small>{
validationErrors.investor_type
}</small>}
      <label>Empresa 
ou
 
instituição
</label>
      <input name="
company_name
" value={
form.company_name
} 
onChange
={
handleChange
} placeholder="Nome da 
empresa
, se 
aplicável
" />
      <label>Email *</label>
      <input type="email" name="email" value={
form.email
} 
onChange
={
handleChange
} placeholder="email@dominio.com" />
      {
validationErrors.email
 && <small>{
validationErrors.email
}</small>}
      <label>Telefone</label>
      <input name="phone" value={
form.phone
} 
onChange
={
handleChange
} placeholder="+258 ..." />
      <label>País</label>
      <input name="country" value={
form.country
} 
onChange
={
handleChange
} placeholder="País de 
origem
" />
      <label>Sector de interesse *</label>
      <select name="
sector_interest
" value={
form.sector_interest
} 
onChange
={
handleChange
}>
        <option value="">
Seleccione
</option>
        {
sectors.map
((sector) => (
          <option key={sector.id || 
sector.slug
} value={sector.name}>{sector.name}</option>
        ))}
      </select>
      {
validationErrors.sector_interest
 && <small>{
validationErrors.sector_interest
}</small>}
      <label>
Capacidade
 
indicativa
 de 
investimento
</label>
      <select name="
investment_capacity
" value={
form.investment_capacity
} 
onChange
={
handleChange
}>
        <option value="">
Seleccione
</option>
        <option value="below_100k">
Até
 USD 100.000</option>
        <option value="100k_500k">USD 100.000 a USD 500.000</option>
        <option value="500k_1m">USD 500.000 a USD 1 
milhão
</option>
        <option value="above_1m">Acima de USD 1 
milhão
</option>
      </select>
      <label>
Mensagem
 *</label>
      <
textarea
 name="message" value={
form.message
} 
onChange
={
handleChange
} rows="5" placeholder="
Descreva
 o 
tipo
 de interesse, 
parceria
 
ou
 
informação
 
pretendida
." />
      {
validationErrors.message
 && <small>{
validationErrors.message
}</small>}
      <label 
className
="checkbox-field">
        <input type="checkbox" name="
privacy_consent
" checked={
form.privacy_consent
} 
onChange
={
handleChange
} />
        
Aceito
 
que
 
os
 meus dados 
sejam
 
tratados
 para 
efeitos
 de 
contacto
 e 
análise
 da 
manifestação
 de interesse.
      </label>
      {
validationErrors.privacy_consent
 && <small>{
validationErrors.privacy_consent
}</small>}
      <button type="submit" disabled={loading}>
        {loading ? 
'A enviar...
' :
 'Enviar manifestação de interesse'}
      </
button
>
    </
form
>
  );
}
2. interestRequestService.js completo
Ficheiro
: 
src
/services/interestRequestService.js
const 
API_URL
 = 
import.meta.env.VITE_API_URL
 || 'http://localhost:8000';
async function 
handleResponse
(response) {
  const data = await 
response.json
().catch(() => ({}));
  if (!
response.ok
) {
    const error = new Error(
data.message
 || 'Erro 
ao
 
processar
 o 
pedido
.');
    
error.status
 = 
response.status
;
    
error.errors
 = 
data.errors
 || null;
    throw error;
  }
  return data;
}
export async function 
createInterestRequest
(payload) {
  const response = await fetch(`${
API_URL
}/
api
/v1/interest-requests`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/
json
',
      Accept: 'application/
json
',
    },
    body: 
JSON.stringify
({
      
opportunity_id
: 
payload.opportunity_id
 || null,
      
investor_name
: 
payload.investor_name
,
      
investor_type
: 
payload.investor_type
,
      
company_name
: 
payload.company_name
 || null,
      email: 
payload.email
,
      phone: 
payload.phone
 || null,
      country: 
payload.country
 || null,
      
sector_interest
: 
payload.sector_interest
,
      
investment_capacity
: 
payload.investment_capacity
 || null,
      message: 
payload.message
,
      
privacy_consent
: Boolean(
payload.privacy_consent
),
    }),
  });
  return 
handleResponse
(response);
}
3. Exemplo de uso do 
ManifestarInteresseForm.jsx
Ficheiro
: 
src
/pages/
OpportunityDetailPage.jsx
import { 
useEffect
, 
useState
 } from 'react';
import 
ManifestarInteresseForm
 from '../components/forms/
ManifestarInteresseForm
';
export default function 
OpportunityDetailPage
() {
  const [opportunity, 
setOpportunity
] = 
useState
(null);
  const [sectors, 
setSectors
] = 
useState
([]);
  const [loading, 
setLoading
] = 
useState
(true);
  
useEffect
(() => {
    async function 
loadData
() {
      try {
        const 
opportunityResponse
 = await fetch(`${
import.meta.env.VITE_API_URL
}/
api
/v1/opportunities/1`);
        const 
sectorsResponse
 = await fetch(`${
import.meta.env.VITE_API_URL
}/
api
/v1/sectors`);
        const 
opportunityData
 = await 
opportunityResponse.json
();
        const 
sectorsData
 = await 
sectorsResponse.json
();
        
setOpportunity
(
opportunityData.data
 || 
opportunityData
);
        
setSectors
(
sectorsData.data
 || 
sectorsData
);
      } finally {
        
setLoading
(false);
      }
    }
    
loadData
();
  }, []);
  if (loading) {
    return <p>A 
carregar
 
oportunidade
...</p>;
  }
  if (!opportunity) {
    return <p>
Oportunidade
 
não
 
encontrada
.</p>;
  }
  return (
    <main 
className
="opportunity-detail-page">
      <section 
className
="opportunity-hero">
        <h1>{
opportunity.title
}</h1>
        <p>{
opportunity.summary
}</p>
        <ul>
          <li><strong>Sector:</strong> {opportunity.sector?.name}</li>
          <li><strong>
Província
:</strong> {
opportunity.province
}</li>
          <li><strong>
Maturidade
:</strong> {
opportunity.maturity_label
}</li>
        </ul>
      </section>
      <section 
className
="interest-section">
        <
ManifestarInteresseForm
 opportunity={opportunity} sectors={sectors} />
      </section>
    </main>
  );
}
Blocos de código 
Laravel
 para oportunidades e sectores
1. Model 
Sector.php
Ficheiro
: app/Models/
Sector.php
<?
php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\
HasFactory
;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\
HasMany
;
class Sector extends Model
{
    use 
HasFactory
;
    protected $fillable = [
        'name',
        'slug',
        'description',
        'icon',
        'image',
        '
is_active
',
        '
sort_order
',
    ];
    protected $casts = [
        '
is_active
' => '
boolean
',
        '
sort_order
' => 'integer',
    ];
    public function 
opportunities(
): 
HasMany
 
   {
        return $this->
hasMany
(Opportunity::
class);
  
  }
    public function 
activeOpportunities
(
): 
HasMany
 
   {
        return $this->
hasMany
(Opportunity::
class)->
where(
'status', 'published');
  
  }
}
2. Model 
Opportunity.php
Ficheiro
: app/Models/
Opportunity.php
<?
php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\
HasFactory
;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\
BelongsTo
;
class Opportunity extends Model
{
    use 
HasFactory
;
    protected $fillable = [
        '
sector_id
',
        '
reference_code
',
        'title',
        'slug',
        'summary',
        'description',
        'province',
        'district',
        '
promoter_name
',
        '
promoter_type
',
        '
investment_amount
',
        'currency',
        '
financing_modality
',
        '
maturity_level
',
        '
maturity_label
',
        '
expected_impact
',
        'status',
        '
is_featured
',
        '
published_at
',
    ];
    protected $casts = [
        '
investment_amount
' => 'decimal:2',
        '
maturity_level
' => 'integer',
        '
is_featured
' => '
boolean
',
        '
published_at
' => 'datetime',
    ];
    public function sector(): 
BelongsTo
    {
        return $this->
belongsTo
(Sector::class);
    }
    public function 
scopePublished
($query)
    {
        return $query->where('status', 'published');
    }
    public function 
scopeFeatured
($query)
    {
        return $query->where('
is_featured
', true);
    }
}
3. Seeder 
SectorSeeder.php
Ficheiro: 
database
/
seeders
/
SectorSeeder.php
<?
php
namespace
 
Database
\
Seeders
;
use App\
Models
\Sector;
use 
Illuminate
\
Database
\
Seeder
;
use 
Illuminate
\
Support
\
Str
;
class
 
SectorSeeder
 
extends
 
Seeder
{
    
public
 
function
 
run
(): 
void
    {
        $
sectors
 = [
            ['
name
' => 'Agronegócio', '
description
' => 'Produção, processamento, logística agrícola e cadeias de valor alimentares.'],
            ['
name
' => 'Energia e Mineração', '
description
' => 'Energia renovável, gás, 
mineração responsável e transição energética.'],
            ['
name
' => 'Turismo', '
description
' => 'Hotelaria, turismo costeiro, turismo cultural e conservação.'],
            ['
name
' => 'Infraestruturas', '
description
' => 'Transportes, logística, parques industriais, água e saneamento.'],
            ['
name
' => 'Economia Azul', '
description
' => 'Pescas, aquacultura, portos, logística marítima e recursos costeiros.'],
            ['
name
' => 'Mercado Financeiro', '
description
' => 'Serviços financeiros, 
fintech
, mercado de capitais e instrumentos de financiamento.'],
        ];
        
foreach
 ($
sectors
 as $
index
 => $sector) {
            Sector::
updateOrCreate
(
                ['
slug
' => 
Str
::
slug
($sector['
name
'])],
                [
                    '
name
' => $sector['
name
'],
                    '
description
' => $sector['
description
'],
                    '
icon
' => 
null
,
                    '
image
' => 
null
,
                    '
is_active
' => 
true
,
                    '
sort_order
' => $
index
 + 1,
                ]
            );
        }
    }
}
4. Seeder 
OpportunitySeeder.php
Ficheiro
: database/seeders/
OpportunitySeeder.php
<?
php
namespace Database\Seeders;
use App\Models\Opportunity;
use App\Models\Sector;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class 
OpportunitySeeder
 extends Seeder
{
    public function run(): void
    {
        $items = [
            ['sector' => '
Agronegócio
', 'title' => '
Unidade
 de 
processamento
 
agrícola
 
em
 Nampula', 'province' => 'Nampula', 'district' => 'Nampula', 'amount' => 2500000, 'modality' => 'Capital 
ou
 joint venture', 'maturity' => 4],
            ['sector' => 'Energia e 
Mineração
', 'title' => 'Parque solar para 
fornecimento
 
empresarial
', 'province' => 'Maputo', 'district' => 'Boane', 'amount' => 5000000, 'modality' => 'PPP 
ou
 
financiamento
 misto', 'maturity' => 3],
            ['sector' => 'Turismo', 'title' => 'Eco-lodge 
costeiro
 com 
componente
 
comunitária
', 'province' => 'Inhambane', 'district' => '
Vilankulo
', 'amount' => 1800000, 'modality' => 'Capital privado', 'maturity' => 2],
            ['sector' => 'Infraestruturas', 'title' => 'Centro 
logístico
 provincial', 'province' => 'Sofala', 'district' => 'Beira', 'amount' => 8500000, 'modality' => '
Concessão
 
ou
 PPP', 'maturity' => 4],
        ];
        foreach ($items as $item) {
            $sector = Sector::where('name', $item['sector'])->first();
            if (!$sector) {
                continue;
            }
            Opportunity::
updateOrCreate
(
                ['slug' => Str::slug($item['title'])],
                [
                    '
sector_id
' => $sector->id,
                    '
reference_code
' => 'INV-
MZ
-' . 
strtoupper
(
substr
(Str::
slug($item['sector']), 0, 3)
) .
 
'-' . 
random_int
(100, 999),
                    '
title
' => $item['
title
'],
                    '
summary
' => 'Oportunidade de investimento estruturada para atrair parceiros nacionais e estrangeiros.',
                    '
description
' => 'Projecto com potencial económico e social, sujeito a validação técnica, financeira, ambiental e institucional.',
                    '
province
' => $item['
province
'],
                    '
district
' => $item['
district
'],
                    '
promoter_name
' => 'Entidade promotora em validação',
                    '
promoter_type
' => 'Pública/Privada',
                    '
investment_amount
' => $item['
amount
'],
                    '
currency
' => 'USD',
                    '
financing_modality
' => $item['
modality
'],
                    '
maturity_level
' => $item['
maturity
'],
                    '
maturity_label
' => $
this
->
maturityLabel
($item['
maturity
']),
                    '
expected_impact
' => 'Geração de emprego, dinamização territorial e aumento da produção local.',
                    'status' => '
published
',
                    '
is_featured
' => 
true
,
                    '
published_at
' => 
now
(),
                ]
            );
        }
    }
    
private
 
function
 
maturityLabel
(
int
 $
level
): 
string
    {
        
return
 match ($
level
) {
            1 => 'Ideia identificada',
            2 => 'Pré-estudo disponível',
            3 => 'Estudo de viabilidade em curso',
            4 => 'Projecto estruturado',
            5 => 'Pronto para investimento',
            6 => 'Em negociação ou implementação',
            
default
 => 'Em validação',
        };
    }
}
5. 
OpportunityController
 com pesquisa e filtros
Ficheiro
: app/Http/Controllers/Api/V1/
OpportunityController.php
<?
php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Opportunity;
use Illuminate\Http\
JsonResponse
;
use Illuminate\Http\Request;
class 
OpportunityController
 extends Controller
{
    public function featured(): 
JsonResponse
    {
        $opportunities = Opportunity::query()
            ->with('
sector:id,name,slug
')
            ->published()
            ->featured()
            ->latest('
published_at
')
            ->limit(6)
            ->get();
        return response()->
json
([
            'data' => $opportunities,
        ]);
    }
    public function search(Request $request): 
JsonResponse
    {
        $validated = $request->validate([
            'q' => ['nullable', 'string', 'max:120'],
            'sector' => ['nullable', 'string', 'max:80'],
            'province' => ['nullable', 'string', 'max:80'],
            '
maturity_level
' => ['nullable', 'integer', 'between:1,6'],
            '
financing_modality
' => ['nullable', 'string', 'max:100'],
            '
min_amount
' => ['nullable', 'numeric', 'min:0'],
            '
max_amount
' => ['nullable', 'numeric', 'min:0'],
            '
per_page
' => ['nullable', 'integer', 'min:1', 'max:50'],
        ]);
        $query = Opportunity::query()
            ->with('
sector:id,name,slug
')
            ->published();
        if (!empty($validated['q'])) {
            $term = $validated['q'];
            $query->where(function ($q) use ($term) {
                $q->where('title', 'like', "%{$term}%")
                  ->
orWhere
('summary', 'like', "%{$term}%")
                  ->
orWhere
('description', 'like', "%{$term}%")
                  ->
orWhere
('
reference_code
', 'like', "%{$term}%")
                  ->
orWhere
('
expected_impact
', 'like', "%{$term}%");
            });
        }
        if (!empty($validated['sector'])) {
            $sector = $validated['sector'];
            $query->
whereHas
('sector', function ($q) use ($sector) {
                $q->where('slug', $sector)->
orWhere
('name', 'like', "%{$sector}%");
            });
        }
        if (!empty($validated['province'])) {
            $query->where('province', $validated['province']);
        }
        if (!empty($validated['
maturity_level
'])) {
            $query->where('
maturity_level
', $validated['
maturity_level
']);
        }
        if (!empty($validated['
financing_modality
'])) {
            $query->where('
financing_modality
', 'like', '%' . $validated['
financing_modality
'
] .
 '%');
      
  }
        if 
(!empty
($validated['
min_amount
'])) {
            $query->
where(
'
investment_amount
', '>=', $validated['
min_amount
']);
      
  }
        if 
(!empty
($validated['
max_amount
'])) {
            $query->
where(
'
investment_amount
', '<=', $validated['
max_amount
']);
      
  }
        $results = $query
            ->
orderByDesc
('
is_featured
')
            ->
orderByDesc
('
published_at
')
            ->paginate($validated['
per_page
'
] ??
 
12);
        
return
 response
()-
>
json
($
results
);
  
  }
}
24. 
StoreInterestRequestRequest.php
Criar o ficheiro 
app/
Http
/
Requests
/
StoreInterestRequestRequest.php
. O 
request
 deve autorizar submissões públicas, validar os campos obrigatórios e normalizar mensagens de erro. Regras principais: 
fullName
 obrigatório entre 3 e 150 caracteres; email obrigatório e válido; 
phone
 opcional até 30 caracteres; 
investorType
 obrigatório com valores controlados; country obrigatório; 
sectorInterest
 obrigatório; 
opportunityReference
 opcional; 
investmentRange
 opcional; 
message
 obrigatória entre 20 e 2000 caracteres; 
privacyConsent
 obrigatório e verdadeiro.
Exemplo de lógica: 
namespace
 App\
Http
\
Requests
; 
class
 
StoreInterestRequestRequest
 
extends
 
FormRequest
; 
authorize
 retorna 
true
; rules devolve 
array
 com validações; 
messages
 personaliza mensagens em português; 
attributes
 define nomes amigáveis dos campos. Para produção, recomenda-se também validar 
captcha
, limitar pedidos por IP e email, e aplicar sanitização adicional no 
controller
 ou 
service
 antes da gravação.
25. 
Migration
 completa para 
interest_requests
Criar
 a migration 
database/migrations/xxxx_xx_xx_xxxxxx_create_interest_requests_table.php
. Campos 
recomendados
: id, 
opportunity_id
 nullable, reference 
única
, 
full_name
, email, phone, 
investor_type
, country, 
sector_interest
, 
opportunity_reference
, 
investment_range
, message, 
privacy_consent
, 
consented_at
, status, 
source_page
, 
ip_address
, 
user_agent
, metadata 
em
 JSON, 
reviewed_at
, 
reviewed_by
 nullable, timestamps e 
softDeletes
.
Índices
 
recomendados
: reference 
único
; email; 
investor_type
; 
sector_interest
; 
opportunity_reference
; status; 
created_at
; e foreign key 
opportunity_id
 com 
cascadeOnUpdate
 e 
nullOnDelete
. 
O campo 
metadata
 pode guardar informações técnicas adicionais, como origem da campanha, idioma seleccionado, navegador ou parâmetros 
UTM
, desde que não substitua campos estruturados essenciais.
26. 
ManifestarInteresseForm.jsx
Criar o ficheiro 
src
/
components
/
forms
/
ManifestarInteresseForm.jsx
. O componente deve usar 
useState
 para gerir os campos do formulário, 
submitInterestRequest
 do homeService.js para enviar os dados à API, validação simples no 
frontend
, estado de 
loading
, mensagens de erro e confirmação de sucesso com a referência gerada pela API.
Campos do formulário: nome completo, email, telefone, tipo de investidor, país, sector de interesse, referência da oportunidade, faixa de investimento, mensagem e consentimento de privacidade. O botão de submissão deve ficar desactivado enquanto o pedido estiver a ser enviado. Após sucesso, apresentar: “Manifestação de interesse registada com sucesso” e a referência recebida, por exemplo IR-2026-000245.
Criar o ficheiro 
app/
Http
/
Requests
/
StoreInterestRequestRequest.php
 para validar manifestações de interesse antes de gravar os dados na tabela 
interest_requests
. A classe deve autorizar pedidos públicos, aplicar regras de validação, personalizar mensagens em português e proteger o formulário contra submissões abusivas ou incompletas.
Estrutura
 
sugerida
:
 namespace App\Http\Requests; usar Illuminate\Foundation\Http\
FormRequest
; 
criar
 a 
classe
 
StoreInterestRequestRequest
 extends 
FormRequest
; 
definir
 
authorize(
) 
como
 true; e 
em
 
rules(
) 
validar
 name, email, phone, 
investor_type
, 
sector_id
, 
opportunity_id
, 
investment_range
, country, 
company_name
, message, 
privacy_consent
 e website 
como
 campo anti-spam.
Regras recomendadas:
 
name
 obrigatório até 150 caracteres; email obrigatório, válido e até 180 caracteres; 
phone
 opcional até 40 caracteres; 
investor_type
 obrigatório com valores individual, empresa, fundo, 
instituicao_financeira
, parceiro ou outro; 
sector_id
 obrigatório e existente em 
sectors
; 
opportunity_id
 opcional e existente em 
opportunities
; 
investment_range
 opcional com valores predefinidos; country obrigatório até 100 caracteres; 
company_name
 opcional até 180 caracteres; 
message
 obrigatória entre 20 e 3000 caracteres; 
privacy_consent
 aceite; website proibido para funcionar como 
honeypot
.
23. 
Migration
 
interest_requests
Criar
 a migration com o 
comando
 
php
 artisan 
make:migration
 
create_interest_requests_table
. 
A tabela deve guardar os dados essenciais do contacto, o perfil do investidor, a oportunidade associada, o sector de interesse, o estado inicial do pedido e metadados úteis para auditoria e triagem.
Campos sugeridos:
 id; 
reference_code
 único; 
name
; email; 
phone
; country; 
company_name
; 
investor_type
; 
sector_id
 como chave estrangeira opcional ou obrigatória conforme a regra de negócio; 
opportunity_id
 como chave estrangeira opcional; 
investment_range
; 
message
; status com valor inicial 
new
; 
source_page
; 
ip_address
; 
user_agent
; 
privacy_consent_at
; 
reviewed_at
; 
reviewed_by
; 
internal_notes
; 
timestamps
 e 
softDeletes
.
Índices
 
recomendados
:
 
indexar
 email, status, 
investor_type
, 
sector_id
, 
opportunity_id
, 
created_at
 e 
reference_code
. 
Estes índices facilitam pesquisa administrativa, acompanhamento do funil de interesse, relatórios por sector e rastreio de pedidos individuais.
24. Exemplo de componente 
React
 Manifestar Interesse
Criar o componente 
src
/
components
/
forms
/
ManifestarInteresseForm.jsx
 para recolher dados do investidor na página principal, numa página dedicada ou dentro de modal aberto a partir dos cards de oportunidade.
Estado 
inicial
 do 
formulário
:
 name, email, phone, country, 
company_name
, 
investor_type
, 
sector_id
, 
opportunity_id
, 
investment_range
, message, 
privacy_consent
 e website. 
O campo website deve permanecer oculto para funcionar como mecanismo simples anti-spam.
Fluxo do componente:
 actualizar o estado a cada alteração de campo; submeter os dados para POST /
api
/v1/
interest-requests
; bloquear o botão durante o envio; apresentar mensagem de sucesso com referência do pedido; e mostrar erros de validação devolvidos pelo 
Laravel
 junto de cada campo.
Campos visíveis recomendados:
 Nome, email, telefone, país, empresa/organização, tipo de investidor, sector de interesse, oportunidade associada quando existir, faixa indicativa de investimento, mensagem e consentimento de privacidade. O botão principal deve chamar-se “Enviar manifestação de interesse”.
Criar o ficheiro 
src
/
services
/homeService.js
 para centralizar todas as chamadas públicas da página principal. O serviço deve usar 
Axios
, variável de ambiente 
VITE_API_URL
, tratamento padronizado de erros e funções reutilizáveis para 
home
, sectores, oportunidades em destaque, pesquisa e manifestações de interesse.
Exemplo de implementação: importar 
axios
; criar 
apiClient
 com 
baseURL
, 
timeout
 e 
headers
 
JSON
; interceptar respostas para devolver 
response.data
; interceptar erros para normalizar mensagens; exportar 
getHomeData
(
locale
 = '
pt
'), 
getSectors
(
locale
 = '
pt
'), 
getFeaturedOpportunities
(
locale
 = '
pt
'), 
searchOpportunities
(
params
 = {}), 
getPortalStats
(
) e 
submitInterestRequest
(
payload
). A função 
submitInterestRequest
 deve enviar POST para /
api
/v1/
interest-requests
 e devolver referência, estado e mensagem de sucesso.
22. 
Migrations
 
Laravel
 para sectores e oportunidades
A migration de 
sectors
 
deve
 
conter
: id, name, slug 
único
, 
short_description
, description, icon, 
image_path
, 
sort_order
, 
is_active
, 
published_at
, timestamps e 
softDeletes
. 
O 
slug
 deve ser indexado para permitir rotas públicas como /sectores/
agronegocio
.
A migration de 
opportunities
 
deve
 
conter
: id, 
sector_id
 
como
 
chave
 
estrangeira
, reference 
único
, title, slug 
único
, summary, description, province, district, 
promoter_name
, 
promoter_type
, 
maturity_level
, 
maturity_label
, 
estimated_investment_amount
, 
estimated_investment_currency
, 
financing_modality
, 
expected_impact
, status, 
is_featured
, 
published_at
, 
last_validated_at
, timestamps e 
softDeletes
. 
A relação com 
sectors
 deve usar foreignId('sector_id
')-
>constrained
()-
>cascadeOnUpdate
()-
>
restrictOnDelete(
).
Índices
 
recomendados
: 
sectors.slug
, 
sectors.is_active
, 
opportunities.reference
, 
opportunities.slug
, 
opportunities.status
, 
opportunities.is_featured
, 
opportunities.province
, 
opportunities.maturity
_level
 e 
opportunities.published
_at
. 
Estes 
índices melhoram a listagem pública, a pesquisa e a filtragem por sector, província, maturidade e destaque.
23. Exemplo de 
InterestRequestController.php
O 
InterestRequestController
 deve receber manifestações de interesse públicas, validar os campos, gerar uma referência interna, gravar o pedido e devolver uma resposta segura. A validação deve ser feita preferencialmente num 
StoreInterestRequestRequest
, separando as regras do 
controller
.
Lógica recomendada: receber 
StoreInterestRequestRequest
; obter dados validados; gerar 
reference
 no formato IR-YYYY-000001; criar registo em 
InterestRequest
; guardar IP, 
user_agent
 e consentimento de privacidade; associar 
opportunity_id
 quando a 
opportunityReference
 existir; devolver 
JsonResponse
 com 
success
 
true
, 
message
, 
data.reference
, 
data.status
, 
data.nextStep
 e 
submittedAt
. Em caso de falha, registar o erro no log e devolver mensagem genérica sem expor detalhes internos.
Regras mínimas de validação: 
fullName
 obrigatório com 3 a 150 caracteres; email obrigatório e válido; 
phone
 opcional até 30 caracteres; 
investorType
 obrigatório entre individual, empresa, fundo, 
entidade_publica
, 
parceiro_tecnico
 e outro; country obrigatório; 
sectorInterest
 obrigatório; 
opportunityReference
 opcional e existente quando informado; 
investmentRange
 opcional; 
message
 obrigatória com 20 a 2000 caracteres; 
privacyConsent
 obrigatório e verdadeiro. Aplicar rate 
limiting
 por IP e email para reduzir spam.
16. Código completo para 
HomePage.jsx
Segue uma implementação completa e funcional para 
src
/
pages
/
HomePage.jsx
, preparada para consumir o 
endpoint
 
/
api
/v1/
home
, gerir estados de carregamento, erro, pesquisa e 
renderização
 condicional dos principais blocos da página principal.
Código sugerido: importar 
React
, 
useEffect
 e 
useState
; definir 
API_BASE_URL
 com 
import.meta.env.VITE_API_URL
; criar estado para 
homeData
, 
loading
, error e 
searchTerm
; carregar dados com 
fetch
(`${
API_BASE_URL
}/
api
/v1/
home?locale
=
pt
`); validar 
response.ok
; guardar 
JSON
 em 
homeData
; apresentar 
loading
 enquanto carrega; apresentar mensagem de erro se falhar; 
renderizar
 
Header
, 
hero
, botões rápidos, pesquisa, sectores estratégicos, oportunidades em destaque, percurso do investidor, incentivos, estatísticas e 
footer
. A navegação dos botões pode usar 
window.location
.href
 ou 
React
 Router conforme a arquitectura adoptada.
17. Exemplo completo para 
HomeController.php
O 
controller
 deve ser colocado em 
app/
Http
/
Controllers
/
Api
/V1/
HomeController.php
. A versão inicial pode devolver um 
payload
 estruturado directamente, mas a versão 
recomendada para produção deve delegar a composição dos dados para um 
HomeService
, permitindo cache, testes unitários e separação da lógica de negócio.
Estrutura lógica recomendada: definir 
namespace
 App\
Http
\
Controllers
\
Api
\V1; importar 
Request
, 
JsonResponse
, Cache e modelos públicos; criar método show(
Request
 $
request
): 
JsonResponse
; validar 
locale
 com valores permitidos 
pt
 e 
en
; aplicar cache por 
locale
 durante 10 a 30 minutos; devolver 
hero
, 
quickActions
, 
searchSuggestions
, 
sectors
, 
featuredOpportunities
, 
investorJourney
, 
incentivesPreview
, 
legalHighlights
, 
partnershipExchange
 e 
stats
; limitar os campos expostos para evitar publicação de dados sensíveis.
18. Payload JSON 
completo
 para /
api
/v1/home
Exemplo completo de resposta pública: { "
success
": 
true
, "
locale
": "
pt
", "data": { "
hero
": { "
title
": "Invista em Moçambique com informação clara, oportunidades estruturadas e apoio especializado", "
subtitle
": "Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas.", "
primaryButton
": { "
label
": "Explorar oportunidades", "
url
": "/oportunidades" }, "
secondaryButton
": { "
label
": "Como investir", "
url
": "/guia-do-investidor" } }, "
quickActions
": [ { "
label
": "Explorar oportunidades", "
url
": "/oportunidades", "
type
": "
primary
" }, { "
label
": "Ver incentivos", "
url
": "/incentivos", "
type
": "
secondary
" }, { "
label
": "Manifestar interesse", "
url
": "/manifestar-interesse", "
type
": "
secondary
" }, { "
label
": "Área do investidor", "
url
": "/login", "
type
": "
outline
" } ], "
searchSuggestions
": [ "registo na APIEX", "benefícios fiscais", "Zonas Económicas Especiais", "repatriamento de dividendos", "NUIT", "licenciamento simplificado" ], "
sectors
": [ { "
name
": "Agronegócio", "
slug
": "
agronegocio
", "
description
": "Oportunidades em produção, 
agroprocessamento
, irrigação e cadeias de valor agrícolas.", "
opportunitiesCount
": 12, "
url
": "/sectores/
agronegocio
" }, { "
name
": "Energia e Mineração", "
slug
": "energia-
mineracao
", "
description
": "Projectos ligados a energias renováveis, transição energética, mineração responsável e infraestruturas associadas.", "
opportunitiesCount
": 9, "
url
": "/sectores/energia-
mineracao
" } ], "
featuredOpportunities
": [ { "
reference
": "INV-MZ-AGR-001", "
title
": "Unidade de processamento agrícola", "sector": "Agronegócio", "
province
": "Nampula", "
maturity
": "Projecto estruturado", "
estimatedInvestment
": { "
amount
": 2500000, "
currency
": "USD" }, "
impact
": "Geração de emprego local, valorização da produção agrícola e substituição de importações.", "
url
": "/oportunidades/inv-mz-agr-001" } ], "
investorJourney
": [ { "step": 1, "
title
": "Identificar oportunidade", "
description
": "Pesquisar sectores, províncias, oportunidades e requisitos iniciais." }, { "step": 2, "
title
": "Analisar requisitos", "
description
": "Consultar requisitos legais, fiscais, cambiais e sectoriais." }, { "step": 3, "
title
": "Manifestar interesse", "
description
": "Submeter contacto inicial ou pedido de informação ao portal." } ], "
incentivesPreview
": [ { "
title
": "Benefícios fiscais", "
description
": "Informação orientadora sobre benefícios fiscais aplicáveis por sector e localização.", "
url
": "/incentivos/
beneficios
-fiscais" }, { "
title
": "ZEE e 
ZFI
", "
description
": 
"Resumo dos regimes especiais aplicáveis a Zonas Económicas Especiais e Zonas Francas Industriais.", "
url
": "/incentivos/
zee-zfi
" } ], "
legalHighlights
": [ { "
title
": "Guia de investimento privado", "
category
": "Investimento", "
url
": "/recursos/guia-investimento-privado" }, { "
title
": "Guia de repatriamento de capitais", "
category
": "Regulação cambial", "
url
": "/recursos/repatriamento-capitais" } ], "
partnershipExchange
": { "
title
": "Bolsa de Parcerias, Investidores e Capital", "
description
": "Ligue-se a promotores, investidores, empresas e parceiros institucionais.", "
actions
": [ { "
label
": "Procurar parceiro", "
url
": "/parcerias" }, { "
label
": "Submeter projecto", "
url
": "/submeter-projecto" } ] }, "
stats
": { "
opportunities
": 48, "
sectors
": 6, "
provinces
": 11, "
interestRequests
": 120, "
updatedAt
": "2026-08-27" } } }
19. 
Payload
 
JSON
 para manifestação de interesse
Exemplo de pedido POST para 
/
api
/v1/
interest-requests
: 
{ "
fullName
": "Nome do Investidor", "email": "investidor@empresa.com", "
phone
": "+258 84 000 0000", "
investorType
": "empresa", "country": "Moçambique", "
sectorInterest
": "Agronegócio", "
opportunityReference
": "INV-MZ-AGR-001", "
investmentRange
": "1000000-5000000", "
message
": "Pretendo receber mais informação sobre a oportunidade e os requisitos aplicáveis.", "
privacyConsent
": 
true
 }
20. 
Payload
 
JSON
 de resposta a manifestação de interesse
Exemplo de resposta: 
{ "
success
": 
true
, "
message
": "Manifestação de interesse registada com sucesso.", "data": 
{ "
reference
": "IR-2026-000245", "status": "
received
", "
nextStep
": "A equipa do portal analisará o pedido e poderá solicitar informação adicional.", "
submittedAt
": "2026-08-27T10:34:00+02:00
" }
 }
Fluxo recomendado:
 o método show recebe a 
request
, valida o parâmetro 
locale
, chama 
HomeService
::
getHomePayload
($
locale
) e devolve resposta 
JSON
 padronizada com status, data e eventuais mensagens.
16. Validação com 
Laravel
 
Form
 
Requests
Para manter 
controllers
 limpos e regras reutilizáveis, os formulários públicos devem usar 
Form
 
Requests
. Os exemplos prioritários para a 
home
 são: 
StoreInterestRequest
, 
StoreContactRequest
 e 
SearchOpportunityRequest
.
Form
 
Request
Campos principais
Regras recomendadas
StoreInterestRequest
name, email, phone, 
investor_type
, 
sector_id
, 
opportunity_id
, message, 
privacy_consent
Nome obrigatório; email válido; telefone opcional; tipo de investidor em lista fechada; sector existente; oportunidade opcional e existente; mensagem 
limitada; consentimento obrigatório.
StoreContactRequest
name, email, subject, message, 
preferred_contact_method
Nome e email obrigatórios; assunto obrigatório; mensagem entre limites mínimos e máximos; método de contacto em lista controlada.
SearchOpportunityRequest
q, sector, province, maturity, 
financing_type
, page, 
per_page
Texto de pesquisa limitado; filtros opcionais e validados; paginação obrigatória; limite máximo por página para evitar abusos.
17. Exemplo prático de regras de validação
Para 
StoreInterestRequest
, as regras devem exigir: 
name
 como obrigatório e texto até 150 caracteres; email obrigatório e válido; 
phone
 como texto opcional até 30 caracteres; 
investor_type
 obrigatório com valores como individual, empresa, fundo, instituição financeira ou parceiro; 
sector_id
 obrigatório e existente na tabela 
sectors
; 
opportunity_id
 opcional e existente na tabela 
opportunities
; 
message
 obrigatória com limite razoável; 
privacy_consent
 aceite; e campos ocultos anti-spam rejeitados quando preenchidos.
Para 
SearchOpportunityRequest
, o campo 
q
 deve ser opcional, textual e limitado; sector e 
province
 devem corresponder a 
slugs
 existentes; 
maturity
 deve pertencer a uma lista fechada de estágios; 
financing_type
 deve aceitar apenas modalidades previstas; 
page
 deve ser inteiro positivo; e 
per_page
 deve ter limite máximo, por exemplo 24 resultados por pedido.
18. Recomendações de arranque para implementação
Criar primeiro a 
home
 em 
React
 com dados 
mockados
 para validar layout, botões e experiência do utilizador.
Implementar em seguida o 
endpoint
 
/
api
/v1/
home
 em 
Laravel
 com 
payload
 fixo ou 
seeders
 iniciais.
Separar chamadas específicas para pesquisa, oportunidades e formulários apenas quando a 
home
 estiver visualmente estável.
Adicionar 
Form
 
Requests
 antes de expor formulários públicos em ambiente de teste.
Aplicar rate 
limiting
, 
logs
 e protecção anti-spam desde a primeira versão pública.
Preparar testes de API para 
home
, pesquisa e manifestações de interesse.
7. Estrutura mínima de resposta 
JSON
 para a 
home
O 
endpoint
 
/
api
/v1/
home
 deve devolver uma resposta agregada para reduzir chamadas iniciais no carregamento da página, incluindo: dados do 
hero
, botões principais, sectores, oportunidades em destaque, indicadores, blocos de legislação, resumo da bolsa de parcerias e configurações de idioma.
8. Recomendações técnicas iniciais
Usar 
React
 com componentes reutilizáveis, organizados por domínio funcional: layout, oportunidades, sectores, formulários e indicadores.
No 
Laravel
, criar 
controllers
 separados para 
Home
, Sectores, Oportunidades, Incentivos, Recursos Legais, Manifestações de Interesse e Estatísticas Públicas.
Utilizar paginação e filtros nos endpoints de oportunidades para evitar carregamentos pesados.
Guardar textos da 
home
 em base de dados ou ficheiros de configuração para permitir actualização sem alteração de código.
Preparar a arquitectura para suporte multilingue desde a primeira versão, mesmo que a versão inicial publique apenas português.
Separar claramente conteúdos públicos de dados reservados ao investidor autenticado.
Aplicar validação, rate 
limiting
, 
logs
 e protecção contra spam nos formulários públicos.
Conclusão estratégica:
 As recomendações devem ser implementadas de forma faseada, começando pelas acções de maior impacto imediato na previsibilidade, 
na 
qualidade documental e
 na
 coordenação institucional. A médio e longo prazo, a prioridade deve ser consolidar a monitoria digital, a integração entre entidades e o suporte pós-investimento como pilares permanentes da proposta de valor do portal.
Indicadores de risco
Objectivo dos indicadores de risco:
 Estes indicadores permitem identificar, de forma preventiva, factores que podem comprometer a eficiência do ciclo de vida do investidor, a credibilidade do portal, a articulação institucional e a concretização efectiva dos projectos de investimento.
Categoria de risco
Indicador de risco
Sinal de alerta
Nível de risco
Responsável pela mitigação
Acção preventiva recomendada
Institucional
Atraso na emissão de 
pareceres ou validações
Processos parados por 
mais de 15 dias úteis sem actualização formal.
Alto
Entidade coordenadora
; APIEX; entidades sectoriais
Definir pontos focais, prazos de resposta e 
regras de escalonamento interinstitucional.
Documental
Submissões incompletas ou inconsistentes
Elevada taxa de devolução por falta de documentos, dados divergentes ou anexos inválidos.
Médio/Alto
Investidor; portal; consultores técnicos
Disponibilizar checklists obrigatórias, validação prévia e modelos normalizados de dossier.
Regulatório
Informação legal desactualizada no portal
Conteúdos sobre incentivos, licenciamento ou repatriamento sem revisão periódica.
Alto
Equipa jurídica; entidades competentes
Instituir calendário de revisão legal e validação institucional antes de cada publicação.
Tecnológico
Falhas de disponibilidade, segurança ou desempenho do portal
Interrupções frequentes, lentidão, falhas de autenticação ou incidentes de segurança.
Alto
Equipa técnica do portal
Implementar monitoria de disponibilidade, cópias de segurança, testes de segurança e plano de resposta a incidentes.
Operacional
Baixa taxa de actualização da carteira de oportunidades
Projectos sem actualização de estado, entidade promotora, localização ou fase de maturidade.
Médio
Portal do Investidor; entidades promotoras
Definir periodicidade de actualização, responsáveis por sector e critérios mínimos de publicação.
Financeiro
Desalinhamento entre montante anunciado e capacidade real de mobilização
Oportunidades publicadas sem informação sobre fonte de financiamento, estágio de preparação ou viabilidade.
Médio/Alto
Entidade promotora; investidor; parceiros financeiros
Exigir ficha mínima de viabilidade, fase do projecto e estimativa de financiamento necessário.
Reputacional
Percepção de baixa transparência ou falta de resposta ao investidor
Reclamações recorrentes, ausência de feedback ou processos sem rastreabilidade.
Alto
Entidade facilitadora; equipa do portal
Criar mecanismo de feedback, número único de acompanhamento e relatórios periódicos de resposta.
Pós-investimento
Baixa resolução de constrangimentos após aprovação
Projectos aprovados sem acompanhamento de execução, licenças, marcos físicos ou bloqueios operacionais.
Alto
Entidade facilitadora; APIEX; entidades licenciadoras
Institucionalizar unidade de apoio pós-investimento e painel de monitoria de constrangimentos.
Nota de gestão de risco:
 Os riscos classificados como altos devem ser acompanhados em reuniões periódicas de gestão do portal e integrados no painel de decisão executiva. A monitoria deve combinar indicadores quantitativos, registos de incidentes, feedback dos investidores e validação regular p
or parte d
as entidades competentes.
Bolsa de parcerias, investidores e capital
.
Objectivo da secção:
 
Criar no portal uma área dedicada à ligação entre potenciais investidores, empresas nacionais ou estrangeiras que procuram 
uma 
associação estratégica, projectos que necessitam de financiamento e entidades interessadas em aportar capital, tecnologia, mercado, gestão ou capacidade operacional
.
 
A Bolsa de Parcerias, Investidores e Capital deve funcionar como um mecanismo de aproximação empresarial, permitindo que o portal vá além da 
mera 
divulgação de oportunidades e passe a apoiar a formação de consórcios, 
joint
 ventures, parcerias comerciais, captação de capital e estruturação de projectos de investimento.
Perfis de utilizadores e necessidades
Perfil
Necessidade principal
Tipo de ligação procurada
Resultado esperado
Potencial investidor
Identificar empresas, projectos ou sectores com potencial de retorno e enquadramento legal claro.
Participação societária, parceria estratégica, financiamento ou aquisição parcial.
Oportunidades qualificadas para análise, contacto e negociação.
Empresa que procura capital
Captar financiamento para expansão, modernização, exportação, activos produtivos ou capital circulante.
Investimento directo, crédito estruturado, capital de risco ou parceiro financeiro.
Acesso a investidores compatíveis com o perfil do projecto.
Empresa que procura associação
Encontrar parceiro técnico, comercial, logístico, tecnológico ou institucional para crescimento conjunto.
Joint venture, consórcio, representação comercial, distribuição ou transferência de tecnologia.
Formação de parceria com responsabilidades, contribuições e benefícios definidos.
Promotor de projecto
Estruturar uma oportunidade ainda em fase inicial para torná-la atractiva ao mercado.
Assistência técnica, co-desenvolvimento, 
pré-investimento
 ou preparação de dossier.
Projecto melhor estruturado e pronto para apresentação a investidores.
Entidade facilitadora
Validar perfis, reduzir assimetria de informação e apoiar contactos qualificados.
Mediação, triagem, encaminhamento e acompanhamento de manifestações de interesse.
Processo mais transparente, rastreável e orientado a resultados.
Fluxo operacional de ligação
Registo do perfil do investidor, 
da 
empresa ou 
do 
promotor de projecto.
Submissão de informação mínima sobre 
o sector, a localização, o montante pretendido, o tipo de parceria, o estágio do projecto e a documentação
 disponível.
Triagem preliminar pela entidade facilitadora para verificar 
a 
consistência, 
a 
elegibilidade e
 a
 qualidade da informação.
Classificação da oportunidade por sector, província, dimensão financeira, maturidade e tipo de capital ou 
de 
associação procurada.
Identificação automática ou assistida de potenciais correspondências entre investidores e empresas.
Manifestação de interesse por uma das partes, com registo electrónico e notificação à contraparte.
Agendamento de reuni
ões
, partilha controlada de documentos e assinatura de termo de confidencialidade
,
 quando aplicável.
Acompanhamento da negociação, registo do estado da oportunidade e reporte de resultados.
Matriz funcional da bolsa de parcerias
Funcionalidade
Descrição
Beneficiário principal
Resultado esperado
Cadastro qualificado de oportunidades
Registo padronizado de empresas, projectos e necessidades de capital, incluindo sector, localização, montante, estágio e documentos disponíveis.
Empresas e promotores
Base de oportunidades mais organizada, comparável e atractiva.
Perfil do investidor
Registo do interesse por sector, província, ticket mínimo, horizonte de investimento, apetite ao risco e tipo de participação pretendida.
Investidores
Melhor correspondência entre capital disponível e oportunidades compatíveis.
Motor de correspondência
Cruzamento de critérios entre investidores e empresas com base em sector, montante, localização, maturidade e tipo de parceria.
Investidores e empresas
Redução do tempo de procura e aumento da qualidade dos contactos.
Manifestação de interesse
Funcionalidade para uma parte sinalizar interesse formal numa oportunidade, com registo de data, estado e próximos passos.
Investidores e promotores
Contactos rastreáveis e gestão transparente das interacções.
Sala segura de documentos
Espaço reservado para partilha controlada de planos de negócio, demonstrações financeiras, licenças, estudos e documentos societários.
Empresas, investidores e entidade facilitadora
Maior confiança, segurança documental e preparação para due diligence.
Painel de oportunidades em negociação
Monitoria do estado das interacções: interessado, reunião agendada, due diligence, negociação, acordo assinado ou oportunidade encerrada.
Entidade facilitadora
Visibilidade executiva sobre o funil de parcerias e investimento.
Critérios mínimos de qualificação
Identificação completa da empresa, 
do 
promotor ou 
do 
investidor.
Sector de actividade e localização da oportunidade.
Descrição clara da necessidade: capital, associação, tecnologia, mercado, gestão ou financiamento.
Montante indicativo pretendido ou faixa de investimento esperada.
Estágio do projecto: ideia, estudo preliminar, projecto estruturado, operação existente ou expansão.
Documentação mínima disponível, incluindo plano de negócios, registos, licenças, demonstrações financeiras ou estudo de viabilidade, quando aplicável.
Declaração sobre confidencialidade, autorização de contacto e uso responsável da informação.
Indicadores de monitoria da bolsa
Número de investidores registados por sector e origem geográfica.
Número de empresas e projectos cadastrados na Bolsa.
Volume estimado de capital procurado e 
de 
capital disponível declarado.
Número de correspondências geradas pelo sistema ou pela entidade facilitadora.
Número de manifestações de interesse recebidas.
Número de reuniões realizadas entre investidores e empresas.
Número de oportunidades em due diligence, negociação e 
de 
acordo
s
 concluído
s
.
Montante estimado de investimento mobilizado através da Bolsa.
Instruções gerais de preenchimento
Preencher todos os campos assinalados como obrigatórios antes da submissão.
Usar informação oficial, actualizada e coerente com os documentos de suporte.
Indicar valores monetários em USD ou MZN, especificando sempre a moeda utilizada.
Seleccionar apenas 
os 
sectores, províncias e tipos de parceria aplicáveis ao perfil do investidor, 
da 
empresa ou
 do
 projecto.
Anexar documentos legíveis, actualizados e 
nos
 formatos aceites pela plataforma.
Evitar abreviaturas, designações comerciais incompletas ou contactos genéricos sem responsável identificado.
Confirmar a autorização para 
o 
contacto, 
a 
triagem e
 o
 uso responsável dos dados antes da submissão.
Nota operacional:
 O sistema deve permitir guardar o formulário como rascunho, validar 
os 
campos em tempo real, destacar
 os
 erros antes da submissão e gerar um comprovativo electrónico com 
um 
número de referência após o envio
.
Validação automática dos formulários
Campo ou área
Regra de validação automática
Mensagem de erro sugerida
Acção do sistema
Campos obrigatórios
Não permitir submissão se campos assinalados como obrigatórios estiverem vazios.
Este campo é obrigatório para concluir a inscrição.
Bloquear avanço ou submissão até preenchimento válido.
Nome do investidor, 
empresa ou promotor
Exigir pelo menos dois caracteres e impedir apenas números ou símbolos.
Indique uma designação válida do investidor, empresa ou promotor.
Destacar campo e solicitar correcção.
E-mail de contacto
Validar formato de e-mail com domínio e extensão reconhecíveis.
Introduza um endereço de e-mail válido.
Impedir submissão até correcção.
Telefone
Permitir indicativo internacional, números e espaços; exigir comprimento mínimo definido.
Introduza um número de telefone válido, incluindo indicativo quando aplicável.
Emitir alerta e solicitar confirmação.
NUIT ou número de registo
Quando aplicável, validar comprimento, formato numérico e ausência de caracteres inválidos.
O NUIT ou número de registo informado não apresenta formato válido.
Marcar como pendente de confirmação documental.
Montante pretendido ou faixa de investimento
Exigir valor positivo, moeda indicada e coerência com a faixa seleccionada.
Informe um montante válido e indique a moeda correspondente.
Bloquear submissão se o valor for zero, negativo ou sem moeda.
Sector de actividade
Exigir selecção a partir de lista padronizada e permitir “outro” apenas com descrição complementar.
Seleccione um sector ou descreva claramente a opção “outro”.
Solicitar preenchimento adicional se “outro” for escolhido.
Localização do projecto
Exigir província e, quando possível, distrito ou área de implementação.
Indique pelo menos a província do projecto ou área de interesse.
Bloquear classificação territorial até preenchimento mínimo.
Tipo de parceria pretendida
Validar se foi seleccionado pelo menos um tipo de parceria, capital ou financiamento.
Seleccione o tipo de ligação pretendida para permitir a correspondência.
Impedir geração de correspondências sem categoria definida.
Documentos anexados
Validar formatos aceites, tamanho máximo, legibilidade do nome do ficheiro e ausência de extensões proibidas.
O documento anexado não cumpre os requisitos de formato ou tamanho.
Rejeitar o ficheiro e solicitar novo anexo.
Declaração de consentimento
Exigir confirmação expressa antes de permitir submissão final.
É necessário aceitar a declaração de consentimento e confidencialidade.
Bloquear submissão final.
Duplicação de registo
Verificar coincidência de e-mail, NUIT, telefone ou nome da entidade já registada.
Foi identificado um registo semelhante. Confirme se pretende actualizar ou continuar.
Gerar alerta e encaminhar para validação manual, quando necessário.
Resultado esperado da validação automática:
 reduzir erros de preenchimento, melhorar a qualidade dos dados, acelerar a triagem, facilitar a correspondência entre investidores e empresas e aumentar a confiança no processo de inscrição da Bolsa de Parcerias, Investidores e Capital.
Formulário de inscrição: investidores
Finalidade:
 Recolher informação mínima sobre investidores interessados em identificar empresas, projectos ou oportunidades compatíveis com o seu perfil financeiro, sectorial e estratégico.
Campo
Informação a preencher
Obrigatório
Nome do investidor ou entidade investidora
________________________________________
Sim
Tipo de investidor
Individual; empresa; fundo; instituição financeira; parceiro estratégico; outro.
Sim
País de origem
________________________________________
Sim
Pessoa de contacto
Nome, cargo, e-mail e telefone.
Sim
Sectores de interesse
Agronegócio; energia; mineração; turismo; infraestruturas; indústria; tecnologia; serviços; outro.
Sim
Províncias ou regiões de interesse
________________________________________
Não
Faixa indicativa de investimento
Até USD 100 mil; USD 100 mil–500 mil; USD 500 mil–1 milhão; USD 1–5 milhões; acima de USD 5 milhões.
Sim
Tipo de participação pretendida
Capital social; financiamento; 
joint
 venture; aquisição parcial; parceria técnica; representação; outro.
Sim
Horizonte de investimento
Curto prazo; médio prazo; longo prazo.
Não
Apetite ao risco
Baixo; moderado; elevado; a definir após análise.
Não
Documentos disponíveis
Perfil institucional; comprovativo de capacidade financeira; carta de intenção; mandato; outro.
Não
Observações adicionais
________________________________________
Não
Formulário de inscrição: empresas e promotores
Finalidade: Recolher informação mínima sobre empresas, promotores ou projectos que procuram 
uma 
associação estratégica, investimento, financiamento, capital, tecnologia, mercado ou parceiros operacionais.
Campo
Informação a preencher
Obrigatório
Nome da empresa ou promotor
________________________________________
Sim
NUIT ou número de registo
________________________________________
Sim, quando aplicável
Pessoa de contacto
Nome, cargo, e-mail e telefone.
Sim
Sector de actividade
________________________________________
Sim
Localização do projecto ou empresa
Província, distrito e localidade.
Sim
Estágio do projecto
Ideia; estudo preliminar; projecto estruturado; empresa em operação; expansão; recuperação; outro.
Sim
Necessidade principal
Capital; financiamento; associação; tecnologia; mercado; gestão; parceiro técnico; distribuição; outro.
Sim
Montante pretendido
Valor indicativo em USD ou MZN.
Sim
Tipo de parceria pretendida
Joint venture; participação societária; crédito; representação; consórcio; parceria comercial; outro.
Sim
Descrição resumida da oportunidade
Objectivo, produto ou serviço, mercado-alvo, capacidade instalada e potencial de crescimento.
Sim
Documentos disponíveis
Plano de negócios; estudo de viabilidade; demonstrações financeiras; licenças; certidões; apresentação institucional; outro.
Não
Estado de preparação para due diligence
Pronto; parcialmente pronto; necessita apoio técnico; a definir.
Não
Observações adicionais
________________________________________
Não
Declaração de consentimento e confidencialidade
Ao submeter o formulário, o investidor, empresa ou promotor declara que as informações fornecidas são verdadeiras, autoriza o uso dos dados para fins de triagem, correspondência e contacto qualificado no âmbito da Bolsa de Parcerias, Investidores e Capital, e reconhece que a partilha de informação não substitui processos de due diligence, validação jurídica, análise financeira ou negociação contratual entre as partes.
Campos de confirmação sugeridos:
 Autorizo o contacto pela entidade facilitadora; aceito a utilização responsável dos dados para efeitos de correspondência; confirmo que posso apresentar documentação de suporte quando solicitada; aceito que informações sensíveis só sejam partilhadas mediante consentimento e, quando aplicável, 
mediante 
termo de confidencialidade.
Nota de governação:
 A Bolsa de Parcerias, Investidores e Capital deve operar com regras claras de confidencialidade, validação mínima de informação, protecção de dados, prevenção de conflitos de interesse e separação entre 
a 
facilitação institucional e
 a
 decisão privada de investimento. O portal deve facilitar contactos qualificados, mas não substituir a due diligence, a negociação contratual ou a decisão autónoma das partes.
Indicadores de acompanhamento recomendados
Número de oportunidades publicadas e actualizadas no portal.
Número de manifestações de interesse recebidas por sector e província.
Tempo médio entre a submissão inicial e a primeira resposta institucional.
Número de projectos encaminhados para análise pelas entidades competentes.
Percentagem de processos com documentação completa na primeira submissão.
Montante estimado de investimento mobilizado por sector, província e fase do projecto.
Número de constrangimentos reportados e resolvidos durante a implementação.
Taxa de acompanhamento pós-investimento e 
de 
expansão de projectos existentes.
Nota executiva final
Este anexo deve ser tratado como 
um 
instrumento operacional de alinhamento entre a experiência do investidor, os processos institucionais e as funcionalidades digitais do portal. A sua implementação permitirá transformar o Portal do Investidor numa 
ferramenta mais orientada 
para a
 facilitação, 
a 
transparência,
 o
 acompanhamento e 
a 
tomada de decisão.
Página
 Principal
 
Para estruturar essa parte no 
Portal do Investidor
, 
é recomend
á
vel 
transformar a “
Carteira de Projectos Prioritários
” numa 
base de dados navegável
, com filtros, páginas de detalhe e uma ficha padronizada p
ara cada
 oportunidade.
Estrutura sugerida:
Página principal: “Oportunidades de Investimento”
Campo de pesquisa livre.
Filtros laterais ou superiores por: 
Sector:
 agronegócio, energia, mineração, turismo, infraestruturas, indústria, economia azul, serviços financeiros, etc.
Província:
 Maputo, Gaza, Inhambane, Sofala, Manica, Tete, Zambézia, Nampula, Cabo Delgado, Niassa.
Estágio de maturidade:
 ideia, estudo preliminar, estudo de viabilidade, pronto para investimento, em estruturação, em implementação.
Entidade promotora:
 Governo, município, empresa pública, empresa privada, associação, cooperativa, PPP
 ou
 parceiro internacional.
Modalidade de financiamento:
 investimento directo, parceria público-privada, concessão, crédito, capital de risco, co-financiamento, doação, leasing ou financiamento misto.
Impacto esperado:
 emprego, exportações, substituição de importações, desenvolvimento local, receitas fiscais, inclusão produtiva, sustentabilidade ambiental.
Cards resumidos de cada oportunidade
:
 Cada oportunidade deve aparecer como um cartão com informação essencial:
Nome do projecto.
Sector.
Província/distrito.
Valor estimado do investimento.
Estágio de maturidade.
Entidade promotora.
Tipo de financiamento pretendido.
Impacto principal esperado.
Bot
ões
: 
“Ver oportunidade”
, 
“Manifestar interesse”
 ou 
“Solicitar informação”
.
Página individual da 
oportunidade
 cada
 projecto deve ter uma ficha completa com:
Descrição do projecto.
Localização.
Justificação económica.
Estado de 
prepara
ção
.
Documentos disponíveis.
Requisitos legais e institucionais.
Incentivos aplicáveis.
Necessidade de financiamento.
Perfil do investidor pretendido.
Entidade responsável.
Contacto ou formulário de manifestação de interesse.
Indicadores de impacto esperado.
Classificação por 
maturidade:
 é
 importante evitar publicar todas as oportunidades como se estivessem prontas para investimento. Sugiro esta classificação:
Nível 1
 — Ideia/Oportunidade identificada
Nível 2
 — Pré-estudo disponível
Nível 3
 — Estudo de viabilidade em curso
Nível 4
 — Projecto estruturado
Nível 5
 — Pronto para captação de investimento
Nível 6
 — Em negociação ou implementação
Impacto esperado como critério de decisão
 O portal deve permitir filtrar oportunidades também pelo impacto:
Empregos previstos.
Montante de investimento mobilizável.
Potencial de exportação.
Benefícios para comunidades locais.
Contribuição para infraestruturas.
Potencial de receitas fiscais.
Alinhamento 
às
 prioridades nacionais ou provinciais.
Modelo de ficha mínima de oportunidade
 A plataforma deve exigir que cada projecto só seja publicado se tiver, pelo menos:
Sector.
Província e distrito.
Entidade promotora.
Descrição curta.
Estágio de maturidade.
Valor estimado.
Modalidade de financiamento pretendida.
Impacto esperado.
Pessoa ou entidade de contacto.
Data da última actualização.
Governança editorial
:
 Para garantir credibilidade, cada oportunidade deve indicar:
Data de publicação.
Data da última actualização.
Fonte da informação.
Entidade responsável pela validação.
Estado: activo, em revisão, suspenso, concluído ou em negociação.
Em termos práticos, a arquitectura ideal seria:
Oportunidades de Investimento → Filtros → Cards → Ficha do Projecto → Manifestação de Interesse → Acompanhamento
Comparação de oportunidades
 
Para tornar
 o portal mais útil para investidores, 
é 
necessário permitir comparar oportunidades, perceber rapidamente o grau de preparação de cada projecto e tomar decisões com base na localização, no sector, no financiamento e no impacto
 esperado.
T
ransformar a “Carteira de Projectos Prioritários
” numa 
base de dados navegável
, com filtros, páginas de detalhe e uma ficha padronizada p
ara cada
 oportunidade.
Estrutura sugerida:
Página principal: “Oportunidades de Investimento”
Campo de pesquisa livre.
Filtros laterais ou superiores por: 
Sector:
 agronegócio, energia, mineração, turismo, infraestruturas, indústria, economia azul, serviços financeiros, etc.
Província:
 Maputo, Gaza, Inhambane, Sofala, Manica, Tete, Zambézia, Nampula, Cabo Delgado, Niassa.
Estágio de maturidade:
 ideia, estudo preliminar, estudo de viabilidade, pronto para investimento, em estruturação, em implementação.
Entidade promotora:
 Governo, município, empresa pública, empresa privada, associação, cooperativa, PPP
 ou
 parceiro internacional.
Modalidade de financiamento:
 investimento directo, parceria público-privada, concessão, crédito, capital de risco, co-financiamento, doação, leasing ou financiamento misto.
Impacto esperado:
 emprego, exportações, substituição de importações, desenvolvimento local, receitas fiscais, inclusão produtiva, sustentabilidade ambiental.
Cards resumidos de cada oportunidade
:
 Cada oportunidade deve aparecer como um cartão com informação essencial:
Nome do projecto.
Sector.
Província/distrito.
Valor estimado do investimento.
Estágio de maturidade.
Entidade promotora.
Tipo de financiamento pretendido.
Impacto principal esperado.
Bot
ões
: 
“Ver oportunidade”
, 
“Manifestar interesse”
 ou 
“Solicitar informação”
.
Página individual da oportunidade
:
 Cada projecto deve ter uma ficha completa com:
Descrição do projecto.
Localização.
Justificação económica.
Estado de 
prepara
ción
.
Documentos disponíveis.
Requisitos legais e institucionais.
Incentivos aplicáveis.
Necessidade de financiamento.
Perfil do investidor pretendido.
Entidade responsável.
Contacto ou formulário de manifestação de interesse.
Indicadores de impacto esperado.
Classificação por maturidade
:
 É importante evitar publicar todas as oportunidades como se estivessem prontas para investimento. Sugiro esta classificação:
Nível 1 — 
Ideia/Oportunidade identificada
Nível 2 — 
Pré-estudo disponível
Nível 3 — 
Estudo de viabilidade em curso
Nível 4 — 
Projecto estruturado
Nível 5 — 
Pronto para captação de investimento
Nível 6 — 
Em negociação ou implementação
Impacto esperado como critério de decisão
 O portal deve permitir filtrar oportunidades também pelo impacto:
Empregos previstos.
Montante de investimento mobilizável.
Potencial de exportação.
Benefícios para comunidades locais.
Contribuição para infraestruturas.
Potencial de receitas fiscais.
Alinhamento 
às
 prioridades nacionais ou provinciais.
Modelo de ficha mínima de oportunidade
:
 A plataforma deve exigir que cada projecto só seja publicado se tiver, pelo menos:
Sector.
Província e distrito.
Entidade promotora.
Descrição curta.
Estágio de maturidade.
Valor estimado.
Modalidade de financiamento pretendida.
Impacto esperado.
Pessoa ou entidade de contacto.
Data da última actualização.
Governança editorial
:
 Para garantir credibilidade, cada oportunidade deve indicar:
Data de publicação.
Data da última actualização.
Fonte da informação.
Entidade responsável pela validação.
Estado:
 activo, em revisão, suspenso, concluído ou em negociação.
Em termos práticos, a arquitectura ideal seria:
Oportunidades de Investimento → Filtros → Cards → Ficha do Projecto → Manifestação de Interesse → Acompanhamento
Esta estrutura torna o portal mais útil para investidores, 
pois permite comparar oportunidades, avaliar rapidamente o grau de preparação de cada projecto e tomar decisões com base na localização, no sector, no financiamento e no impacto
 esperado.
O
rganizar a 
página principal do Portal do Investidor
 como uma porta de entrada simples, comercial e orientada à 
tomada de 
decisão. A ficha de oportunidades deve funcionar como uma unidade padronizada de informação, permitindo comparar projectos com critérios homogéneos.
Organização da página principal do portal
A. Cabeçalho
Deve conter:
Log
o
tipo do portal.
Menu principal:
 
Oportunidades
, 
Sectores
, 
Províncias
, 
Guia do Investidor
, 
Incentivos
, 
Legislação
, 
Bolsa de Parcerias
, 
Contacto
.
Selector de idioma: 
PT | EN
.
Botões visíveis: 
Ver oportunidades
Submeter projecto
Manifestar interesse
B. Secção principal de entrada
A primeira área deve responder rapidamente a três perguntas do investidor:
Onde investir? Em que sector? Como começar?
Sugestão de texto curto:
Encontre oportunidades de investimento em Moçambique por sector, província, estágio de maturidade, entidade promotora, modalidade de financiamento e 
impacto esperado. Compare projectos, consulte requisitos e manifeste interesse 
de forma simples e orientada.
Botões:
Explorar oportunidades
Consultar guia do investidor
Submeter oportunidade
C. Barra de pesquisa e filtros rápidos
Logo abaixo da introdução, incluir uma barra de pesquisa com exemplos úteis:
Pesquisar por sector, província, projecto, entidade promotora ou modalidade de financiamento.
Filtros rápidos:
Sector
Província
Estágio de maturidade
Entidade promotora
Modalidade de financiamento
Impacto esperado
D. Bloco “Oportunidades em destaque”
Apresentar 6 a 8 cards de oportunidades prioritárias.
Cada card deve mostrar:
Nome da oportunidade.
Sector.
Província.
Valor estimado.
Estágio de maturidade.
Modalidade de financiamento.
Impacto principal.
Botão 
"Ver ficha"
 ou 
"Manifestar interesse"
.
E. Navegação por sector
Criar cards sectoriais:
Agronegócio
Energia
Mineração
Turismo
Infraestruturas
Indústria
Economia Azul
Serviços Financeiros
Logística e Transportes
TIC e Economia Digital
Cada card deve indicar:
Número de oportunidades disponíveis.
Províncias com maior concentração.
Tipo de investimento procurado.
Botão 
Ver oportunidades do sector
.
F. Navegação por província
Criar mapa ou grelha territorial com as 11 províncias:
Maputo Cidade
Maputo Província
Gaza
Inhambane
Sofala
Manica
Tete
Zambézia
Nampula
Cabo Delgado
Niassa
Cada província deve apresentar:
Oportunidades disponíveis.
Sectores prioritários.
Corredores logísticos ou zonas económicas relevantes.
Contacto institucional ou entidade promotora.
G. Secção “Como começar”
Uma área simples com cinco passos:
Identifique uma oportunidade.
Consulte 
os 
requisitos, 
os 
incentivos e
 os
 documentos necessários.
Analise a ficha do projecto.
Manifeste interesse ou solicite informação.
Acompanhe o processo junto da entidade competente.
H. Secção “Bolsa de Parcerias, Investidores e Capital”
Esta área deve permitir:
Registo de investidores.
Registo de empresas que procuram capital.
Registo de projectos que procuram parceiros.
Correspondência entre investidores e oportunidades.
Manifestação de interesse.
Agendamento de reuniões.
I. Indicadores na página principal
Incluir indicadores simples para dar credibilidade:
Oportunidades publicadas.
Sectores cobertos.
Províncias representadas.
Projectos prontos para investimento.
Manifestações de interesse recebidas.
Montante estimado de investimento 
na
 carteira.
Modelo de ficha para oportunidades de investimento
A ficha deve ser uniforme 
em
 todas as o
casiõ
es. Recomendo a seguinte estrutura:
A. Identificação da oportunidade
Campo
Conteúdo
Nome da oportunidade
Designação clara do projecto
Código de referência
Ex.: INV-MZ-AGR-001
Sector
Agronegócio, Energia, Turismo, etc.
Subsector
Ex.: processamento agrícola, energia solar, hotelaria
Província
Localização principal
Distrito/Município
Localização específica
Entidade promotora
Governo, município, empresa pública, privada, cooperativa, PPP
Estado da oportunidade
Activa, em revisão, em negociação, suspensa, concluída
B. Resumo executivo da oportunidade
Texto curto, com 5 a 8 linhas, explicando:
Q
ue é o projecto
?
Onde se localiza
?
Qual problema ou oportunidade resolve
?
Que tipo de investidor procura.
Qual o impacto esperado.
Exemplo:
A oportunidade consiste na instalação de uma unidade de processamento agro-industrial na província da Zambézia, orientada 
para a
 valorização de matérias-primas locais e 
para 
o aumento da capacidade de exportação. O projecto procura 
parceiros financeiros e técnicos para o desenvolvimento, a operação e a expansão comercial, com impacto esperado na criação de emprego, na substituição de importações e no fortalecimento
 da cadeia de valor agrícola.
C. Classificação da maturidade
Nível
Estágio
Significado
1
Ideia identificada
Oportunidade ainda conceptual
2
Pré-estudo disponível
Existe informação preliminar
3
Estudo de viabilidade em curso
Análise técnica ou económica em preparação
4
Projecto estruturado
Dados técnicos e financeiros disponíveis
5
Pronto para investimento
Dossier pronto para investidores
6
Em
 
negociação/implementação
Já há investidores ou execução em curso
D. Dados financeiros
Campo
Conteúdo
Valor estimado do investimento
MZN ou USD
Financiamento pretendido
Capital, crédito, PPP, concessão, co-financiamento
Participação pretendida
Minoritária, maioritária, 
joint
 venture, parceria técnica
Horizonte de retorno esperado
Curto, médio ou longo prazo
Documentos financeiros disponíveis
Plano de negócios, orçamento, estudo de viabilidade
E. Modalidade de financiamento
Opções:
Inve
rsão
 direct
a
.
Parceria público-privada.
Concessão.
Crédito bancário.
Capital de risco.
Co-financiamento.
Doação.
Leasing.
Project 
finance
.
Financiamento misto.
F. Impacto esperado
Dimensão
Indicador sugerido
Emprego
Número estimado de postos de trabalho
Investimento
Valor total mobilizável
Exportações
Potencial de exportação
Substituição de importações
Produtos ou serviços substituíveis
Desenvolvimento local
Benefícios para comunidades locais
Sustentabilidade
Impacto ambiental positivo ou reduzido
Receitas públicas
Potencial de contribuição fiscal
Inclusão
Participação de PME, jovens, mulheres ou produtores locais
G. Requisitos e enquadramento legal
A ficha deve indicar:
Licenças necessárias.
Instituições competentes.
Possíveis incentivos aplicáveis.
Requisitos ambientais.
Requisitos fundiários ou de DUAT, quando aplicável.
Necessidade de registo na APIEX.
Requisitos cambiais, se houver capital estrangeiro.
Importante: incluir sempre nota de validação:
A informação legal e fiscal apresentada é orientadora e deve ser confirmada junto das entidades competentes antes da tomada de decisão.
H. Documentos disponíveis
Classificar documentos por estado:
Documento
Estado
Perfil do projecto
Disponível
Estudo preliminar
Disponível / Não disponível
Estudo de viabilidade
Disponível / Em preparação
Plano de negócios
Disponível / Em preparação
Licenças
Disponível / Parcial / Não aplicável
Informação financeira
Disponível sob solicitação
Mapas ou localização
Disponível
I. Entidade promotora e contactos
Campos mínimos:
Nome da entidade promotora.
Tipo de entidade.
Pessoa de contacto.
Cargo.
E-mail.
Telefone.
Website.
Entidade responsável pela validação da informação.
Data da última actualização.
J. Estado editorial da ficha
Campo
Conteúdo
Data de publicação
Dia/mês/ano
Última actualização
Dia/mês/ano
Fonte da informação
Entidade promotora, estudo, documento oficial
Validado por
Equipa editorial ou entidade competente
Estado da ficha
Publicada, em revisão, arquivada
3. Modelo resumido de card para a página principal
Nome do projecto
: 
Sector:
 Agronegócio
Província:
 Zambézia
Investimento estimado:
 USD 2,5 milhões
Maturidade:
 Projecto estruturado
Financiamento pretendido:
 Joint venture
/
capital
Impacto esperado:
 120 empregos, aumento de exportações e valorização da produção local
Botões:
Ver ficha
Manifestar interesse
Solicitar informação
4. Recomendação prática
A página principal deve ser organizada em torno de três caminhos principais:
Investidor que procura oportunidades
Vai directamente para filtros, sectores e províncias.
Empresa ou entidade que quer submeter projecto
Acesse
 
o 
formulário de cadastro de oportunidade.
Investidor ou empresa que procura parceiro
Vai para 
a 
Bolsa de Parcerias, Investidores e Capital.
Assim, o portal deixa de ser apenas informativo e passa a funcionar como 
plataforma de descoberta, qualificação, contacto e facilitação de investimento
.
Prioridade para a primeira fase em React + Laravel
1. Página principal / landing page
Cabeçalho com logótipo, menu principal e selector de idioma 
PT | EN
.
Hero section com mensagem clara: descoberta de oportunidades, orientação legal e facilitação do investimento.
Botões principais: 
Explorar oportunidades
Como investir em Moçambique
Ver incentivos
Submeter interesse
Aceder à área do investidor
Barra de pesquisa com exemplos ligados ao investimento: “registo na APIEX”, “benefícios fiscais”, “ZEE”, “repatriamento de dividendos”.
2. Blocos principais da home
Sectores estratégicos.
Carteira de oportunidades.
Guia do investidor em etapas.
Legislação e incentivos.
Bolsa de parcerias, investidores e capital.
Área reservada / acompanhamento de processos.
3. Fluxo de 
informação
 Eu
 sugiro estruturar o fluxo assim:
Utilizador entra na página principal.
Escolhe entre procurar oportunidade, consultar guia ou manifestar interesse.
sistema apresenta filtros por sector, província, maturidade, entidade promotora e modalidade de financiamento.
utilizador abre uma ficha de oportunidade.
Pode guardar, solicitar informação, manifestar interesse ou iniciar submissão.
Se avançar, cria conta ou entra na área reservada.
Laravel gere autenticação, perfis, permissões, dados, formulários e estados do processo.
Estrutura técnica recomendada
Frontend — React
Componentes reutilizáveis: 
Header
HeroSection
SearchBar
SectorCard
OpportunityCard
InvestorJourney
CTAButtons
LanguageSwitcher
Footer
Backend — Laravel
Módulos principais: 
autenticação;
gestão de utilizadores;
gestão de oportunidades;
sectores e províncias;
documentos legais;
manifestações de interesse;
submissões;
estados do processo;
painel administrativo.
Base de dados inicial
users
investor_profiles
sectors
provinces
opportunities
opportunity_documents
interest_requests
submissions
process_statuses
legal_resources
Caminho prático para começar
Eu começaria por esta sequência:
Desenhar o wireframe da página principal.
Criar os componentes React da home.
Definir as rotas frontend.
Criar no Laravel os modelos, migrations e endpoints básicos.
Ligar a home às oportunidades reais via API.
Só depois avançar para área reservada e fluxos transaccionais.
Se quiser, posso avançar já para o próximo passo e preparar um 
plano técnico detalhado da página principal
, com layout, secções, botões, rotas React, endpoints Laravel e estrutura de base de dados.
Prioridade para a primeira fase em React + Laravel
1. Página principal / landing page
Cabeçalho com logótipo, menu principal e selector de idioma 
PT | EN
.
Hero section com mensagem clara: descoberta de oportunidades, orientação legal e facilitação do investimento.
Botões principais: 
Explorar oportunidades
Como investir em Moçambique
Ver incentivos
Submeter interesse
Aceder à área do investidor
Barra de pesquisa com exemplos ligados ao investimento: “registo na APIEX”, “benefícios fiscais”, “ZEE”, “repatriamento de dividendos”.
2. Blocos principais da home
Sectores estratégicos.
Carteira de oportunidades.
Guia do investidor em etapas.
Legislação e incentivos.
Bolsa de parcerias, investidores e capital.
Área reservada / acompanhamento de processos.
3. Fluxo de 
informação
 Eu
 sugiro estruturar o fluxo assim:
Utilizador entra na página principal.
Escolhe entre procurar oportunidade, consultar guia ou manifestar interesse.
O sistema apresenta filtros por sector, província, maturidade, entidade promotora e modalidade de financiamento.
O utilizador abre uma ficha de oportunidade.
Pode guardar, solicitar informação, manifestar interesse ou iniciar submissão.
Se avançar, cria conta ou entra na área reservada.
Laravel gere autenticação, perfis, permissões, dados, formulários e estados do processo.
Estrutura técnica recomendada
Frontend — React
Componentes reutilizáveis: 
Header
HeroSection
SearchBar
SectorCard
OpportunityCard
InvestorJourney
CTAButtons
LanguageSwitcher
Footer
Backend — Laravel
Módulos principais: 
autenticação;
gestão de utilizadores;
gestão de oportunidades;
sectores e províncias;
documentos legais;
manifestações de interesse;
submissões;
estados do processo;
painel administrativo.
Base de dados inicial
users
investor_profiles
sectors
provinces
opportunities
opportunity_documents
interest_requests
submissions
process_statuses
legal_resources
Caminho prático para começar
Eu começaria por esta sequência:
Desenhar o wireframe da página principal.
Criar os componentes React da home.
Definir as rotas frontend.
Criar no Laravel os modelos, migrations e endpoints básicos.
Ligar a home às oportunidades reais via API.
Só depois avançar para área reservada e fluxos transaccionais.
Se quiser, posso avançar já para o próximo passo e preparar um 
plano técnico detalhado da página principal
, com layout, secções, botões, rotas React, endpoints Laravel e estrutura de base de dados.
Forneça o seu feedback sobre o BizChat
You said: 
Sim, prepare o plano técnico detalhado da página principal, 
Liste
 os componentes React para a página principal, 
Descreva
 os endpoints Laravel necessários para a home
Agent mode said: 
Copilot
O raciocínio foi concluído em 2 passos
Sim — preparei e inseri no documento uma nova secção intitulada 
“Plano técnico detalhado da página principal”
.
Incluí:
Objectivo funcional da página principal
Papel da home como ponto de entrada para descoberta de oportunidades, consulta de requisitos e manifestação de interesse.
Estrutura recomendada do layout
Header, hero section, pesquisa inteligente, sectores estratégicos, oportunidades em destaque, guia “Como investir”, incentivos, bolsa de parcerias, indicadores e footer.
Botões principais
Explorar oportunidades, 
Como
 investir, 
Ver
 incentivos, 
Manifestar
 interesse, 
Entrar
 na área do investidor 
e Submeter
 projecto.
Componentes React
Listei componentes como HomePage, Header, HeroSection, SmartSearchBar, SectorGrid, OpportunityCard, InvestorJourney, IncentivesPreview, PortalStats, entre outros.
Fluxo de informação
Estruturei o percurso do utilizador desde a entrada na home até à pesquisa, consulta de ficha, manifestação de interesse e registo do pedido no Laravel.
Endpoints Laravel
Incluí endpoints como: 
GET /api/v1/home
GET /api/v1/sectors
GET /api/v1/opportunities/featured
GET /api/v1/opportunities/search
POST /api/v1/interest-requests
POST /api/v1/contact-requests
GET /api/v1/portal-stats
entre outros.
Recomendações técnicas iniciais
Separação de controllers, suporte multilingue, validação de formulários, filtros, paginação, segurança e organização dos conteúdos públicos.
Como próximo passo, posso também preparar uma 
estrutura de pastas React + Laravel
, com nomes de ficheiros, controllers, models, migrations e exemplo de payload JSON para a home.
Forneça o seu feedback sobre o BizChat
You said: 
Prepare estrutura de pastas React + Laravel, 
Forneça
 exemplos de payload JSON para a home, Detalhe validação e segurança nos endpoints
Agent mode said: 
Copilot
O raciocínio foi concluído em 2 passos
Atualizei o documento com uma nova secção técnica complementar, alinhada com o projecto em 
React + Laravel
.
Incluí:
Estrutura de pastas recomendada
Organização para React.
Organização para Laravel.
Separação por domínios funcionais: Home, Sectores, Oportunidades, Recursos Legais, Manifestações de Interesse, Estatísticas e Autenticação.
Exemplo de payload JSON para /api/v1/home
Hero section.
Botões principais.
Sectores.
Oportunidades em destaque.
Indicadores públicos.
Destaques legais.
Validação e segurança dos endpoints
Validações por endpoint.
Rate limiting.
CORS.
Anti-spam
.
Protecção de ficheiros.
Encriptação de dados sensíveis.
Auditoria e controlo de exposição de dados.
Como próximo passo, posso preparar a 
estrutura real de código inicial
, incluindo:
rotas React;
exemplo de HomePage.jsx;
serviços Axios;
rotas api.php em Laravel;
controllers;
form requests;
migrations principais.
Forneça o seu feedback sobre o BizChat
You said: 
Gerar código inicial React para HomePage, 
Criar
 rotas API Laravel para home, 
Exemplificar
 validação com Form Requests
Agent mode said: 
Copilot
O raciocínio foi concluído em 2 passos
Concluído — acrescentei ao documento uma nova secção técnica com foco na implementação inicial da página principal.
Incluí:
Código inicial React para HomePage
Estrutura lógica com carregamento de dados, estado de erro, loading e renderização dos principais componentes da home.
Serviço React para consumir a API
Orientação para criar src/services/homeService.js, usando VITE_API_URL e consumindo /api/v1/home?locale=pt.
Rotas API Laravel para a home
Tabela com rotas como: 
GET /home
GET /sectors
GET /opportunities/featured
GET /opportunities/search
GET /portal-stats
POST /interest-requests
POST /contact-requests
Exemplo de HomeController
Recomendei separar a lógica num HomeService, para manter o controller limpo.
Validação com Form Requests
Incluí exemplos para: 
StoreInterestRequest
StoreContactRequest
SearchOpportunityRequest
Regras práticas de validação
Campos obrigatórios, limites, listas fechadas, validação de existência em tabelas, paginação e protecção 
anti-spam
.
Recomendações de arranque
Começar com dados mockados, depois ligar ao Laravel, adicionar validação, rate limiting, logs e testes de API.
Como próximo passo, posso criar uma secção ainda mais prática com 
blocos de código completos
 para:
HomePage.jsx
homeService.js
routes/api.php
HomeController.php
StoreInterestRequest.php
SearchOpportunityRequest.php
Prioridade para a primeira fase em React + Laravel
1. Página principal / landing page
Cabeçalho com logótipo, menu principal e selector de idioma 
PT | EN
.
Hero section com mensagem clara: descoberta de oportunidades, orientação legal e facilitação do investimento.
Botões principais: 
Explorar oportunidades
Como investir em Moçambique
Ver incentivos
Submeter interesse
Aceder à área do investidor
Barra de pesquisa com exemplos ligados ao investimento: “registo na APIEX”, “benefícios fiscais”, “ZEE”, “repatriamento de dividendos”.
2. Blocos principais da home
Sectores estratégicos.
Carteira de oportunidades.
Guia do investidor em etapas.
Legislação e incentivos.
Bolsa de parcerias, investidores e capital.
Área reservada / acompanhamento de processos.
3. Fluxo de 
informação
 Eu
 sugiro estruturar o fluxo assim:
Utilizador entra na página principal.
Escolhe entre procurar oportunidade, consultar guia ou manifestar interesse.
O sistema apresenta filtros por sector, província, maturidade, entidade promotora e modalidade de financiamento.
O utilizador abre uma ficha de oportunidade.
Pode guardar, solicitar informação, manifestar interesse ou iniciar submissão.
Se avançar, cria conta ou entra na área reservada.
Laravel gere autenticação, perfis, permissões, dados, formulários e estados do processo.
Estrutura técnica recomendada
Frontend — React
Componentes reutilizáveis: 
Header
HeroSection
SearchBar
SectorCard
OpportunityCard
InvestorJourney
CTAButtons
LanguageSwitcher
Footer
Backend — Laravel
Módulos principais: 
autenticação;
gestão de utilizadores;
gestão de oportunidades;
sectores e províncias;
documentos legais;
manifestações de interesse;
submissões;
estados do processo;
painel administrativo.
Base de dados inicial
users
investor_profiles
sectors
provinces
opportunities
opportunity_documents
interest_requests
submissions
process_statuses
legal_resources
Caminho prático para começar
Eu começaria por esta sequência:
Desenhar o wireframe da página principal.
Criar os componentes React da home.
Definir as rotas frontend.
Criar no Laravel os modelos, migrations e endpoints básicos.
Ligar a home às oportunidades reais via API.
Só depois avançar para área reservada e fluxos transaccionais.
Se quiser, posso avançar já para o próximo passo e preparar um 
plano técnico detalhado da página principal
, com layout, secções, botões, rotas React, endpoints Laravel e estrutura de base de dados.
Forneça o seu feedback sobre o BizChat
You said: 
Sim, prepare o plano técnico detalhado da página principal, 
Liste
 os componentes React para a página principal, 
Descreva
 os endpoints Laravel necessários para a home
Agent mode said: 
Copilot
O raciocínio foi concluído em 2 passos
Sim — preparei e inseri no documento uma nova secção intitulada 
“Plano técnico detalhado da página principal”
.
Incluí:
Objectivo funcional da página principal
Papel da home como ponto de entrada para descoberta de oportunidades, consulta de requisitos e manifestação de interesse.
Estrutura recomendada do layout
Header, hero section, pesquisa inteligente, sectores estratégicos, oportunidades em destaque, guia “Como investir”, incentivos, bolsa de parcerias, indicadores e footer.
Botões principais
Explorar oportunidades, 
Como
 investir, 
Ver
 incentivos, 
Manifestar
 interesse, 
Entrar
 na área do investidor 
e Submeter
 projecto.
Componentes React
Listei componentes como HomePage, Header, HeroSection, SmartSearchBar, SectorGrid, OpportunityCard, InvestorJourney, IncentivesPreview, PortalStats, entre outros.
Fluxo de informação
Estruturei o percurso do utilizador desde a entrada na home até à pesquisa, consulta de ficha, manifestação de interesse e registo do pedido no Laravel.
Endpoints Laravel
Incluí endpoints como: 
GET /api/v1/home
GET /api/v1/sectors
GET /api/v1/opportunities/featured
GET /api/v1/opportunities/search
POST /api/v1/interest-requests
POST /api/v1/contact-requests
GET /api/v1/portal-stats
entre outros.
Recomendações técnicas iniciais
Separação de controllers, suporte multilingue, validação de formulários, filtros, paginação, segurança e organização dos conteúdos públicos.
Como próximo passo, posso também preparar uma 
estrutura de pastas React + Laravel
, com nomes de ficheiros, controllers, models, migrations e exemplo de payload JSON para a home.
Forneça o seu feedback sobre o BizChat
You said: 
Prepare estrutura de pastas React + Laravel, 
Forneça
 exemplos de payload JSON para a home, Detalhe validação e segurança nos endpoints
Agent mode said: 
Copilot
O raciocínio foi concluído em 2 passos
Atualizei o documento com uma nova secção técnica complementar, alinhada com o projecto em 
React + Laravel
.
Incluí:
Estrutura de pastas recomendada
Organização para React.
Organização para Laravel.
Separação por domínios funcionais: Home, Sectores, Oportunidades, Recursos Legais, Manifestações de Interesse, Estatísticas e Autenticação.
Exemplo de payload JSON para /api/v1/home
Hero section.
Botões principais.
Sectores.
Oportunidades em destaque.
Indicadores públicos.
Destaques legais.
Validação e segurança dos endpoints
Validações por endpoint.
Rate limiting.
CORS.
Anti-spam
.
Protecção de ficheiros.
Encriptação de dados sensíveis.
Auditoria e controlo de exposição de dados.
Como próximo passo, posso preparar a 
estrutura real de código inicial
, incluindo:
rotas React;
exemplo de HomePage.jsx;
serviços Axios;
rotas api.php em Laravel;
controllers;
form requests;
migrations principais.
Forneça o seu feedback sobre o BizChat
import axios from "axios";
const API_BASE_URL = 
import.meta.env.VITE
_API_URL || "http://localhost:8000";
const apiClient = 
axios.create
({
  baseURL: `${API_BASE_
URL}/
api/v1`,
  timeout: 15000,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    const message =
      error
?.response?.data?.
message ||
      
error?.
message ||
      "Ocorreu um erro ao comunicar com o servidor.";
    return Promise.reject({
      message,
      status: error
?.response?.status
 || 500,
      errors: error
?.response?.data?.
errors || null,
    });
  }
);
export async function 
getHomeData(
locale = "pt") {
  return 
apiClient.get(
"/home", {
    params: 
{ locale
 },
  });
}
export async function 
getSectors(
locale = "pt") {
  return 
apiClient.get(
"/sectors", {
    params: 
{ locale
 },
  });
}
export async function 
getFeaturedOpportunities(
locale = "pt") {
  return 
apiClient.get(
"/opportunities/featured", {
    params: 
{ locale
 },
  });
}
export async function 
searchOpportunities(
params = {}) {
  return 
apiClient.get(
"/opportunities/search", {
    params,
  });
}
export async function 
getPortalStats(
) {
  return apiClient.get("/portal-stats");
}
export async function submitInterestRequest(payload) {
  return 
apiClient.post(
"/interest-requests", payload);
}
export default {
  getHomeData,
  getSectors,
  getFeaturedOpportunities,
  searchOpportunities,
  getPortalStats,
  submitInterestRequest,
};
``
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
export default function 
HomePage(
) {
  const [home, setHome] = useState(null);
  const [locale, setLocale] = useState("pt");
  const [searchTerm, setSearchTerm] = 
useState(
"");
  const [loading, setLoading] = useState(true);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    let active = true;
    async function 
loadHome(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await getHomeData(locale);
        if (active) {
          
setHome(
response.data || response);
        }
      } catch (err) {
        if (active) {
          
setError(err.message
 || "Não foi possível carregar a página principal.");
        }
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadHome(
);
    return () => {
      active = false;
    };
  }, [locale]);
  function handleSearchSubmit(event) {
    
event.preventDefault
();
    const query = searchTerm.trim();
    if 
(!query
) return;
    
window.location
.href = `/oportunidades?query=${encodeURIComponent(query
)}`
;
  }
  if (loading) {
    return (
      <main className="min-h-screen flex items-center justify-center">
        <p>A carregar dados do Portal do Investidor...</p>
      </main>
    );
  }
  if (error) {
    return (
      <main className="min-h-screen flex items-center justify-center">
        <div>
          <h1>Erro ao carregar a página</h1>
          <p>{error}</p>
          <button onClick
={
() => 
window.location
.
reload(
)}>
            Tentar novamente
          </button>
        </div>
      </main>
    );
  }
  if 
(!home
) {
    return null;
  }
  return (
    <main className="min-h-screen bg-gray-50 text-gray-900">
      <header className="bg-white border-b">
        <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
          <a href="/" className="font-bold text-xl text-blue-900">
            INVESTE Moçambique
          </a>
          <nav className="hidden md:flex gap-6 text-sm">
            <a href="/oportunidades">Oportunidades</a>
            <a href="/guia-do-investidor">Como investir</a>
            <a href="/incentivos">Incentivos</a>
            <a href="/recursos">Recursos legais</a>
            <a href="/parcerias">Parcerias</a>
          </nav>
          <div className="flex items-center gap-3">
            <button
              onClick
={
() => setLocale("pt")}
              className
={
locale === "pt
" ?
 "font-bold
" :
 ""}
            >
              PT
            </button>
            <span>|</span>
            <button
              onClick
={
() => setLocale("en")}
              className
={
locale === "en
" ?
 "font-bold
" :
 ""}
            >
              EN
            </button>
            <a
              href="/login"
              className="px-4 py-2 border rounded-lg text-sm"
            >
              Área do investidor
            </a>
          </div>
        </div>
      </header>
      <section className="bg-blue-950 text-white">
        <div className="max-w-7xl mx-auto px-6 py-20 grid md:grid-cols-2 gap-10 items-center">
          <div>
            <h1 className="text-4xl md:text-5xl font-bold leading-tight">
              {
home.hero?.
title}
            </h1>
            <p className="mt-6 text-lg text-blue-100">
              {
home.hero?.
subtitle}
            </p>
            <div className="mt-8 flex flex-wrap gap-4">
              {
home.hero?.
primaryButton && (
                <a
                  href={home.hero.primaryButton.url}
                  className="px-6 py-3 bg-white text-blue-950 rounded-lg font-semibold"
                >
                  {
home.hero
.primaryButton.label}
                </a>
              )}
              {
home.hero?.
secondaryButton && (
                <a
                  href={home.hero.secondaryButton.url}
                  className="px-6 py-3 border border-white rounded-lg font-semibold"
                >
                  {
home.hero
.secondaryButton.label}
                </a>
              )}
            </div>
          </div>
          <div className="bg-white/10 rounded-2xl p-6">
            <h2 className="text-xl font-semibold mb-4">
              Pesquisa rápida
            </h2>
            <form onSubmit={handleSearchSubmit} className="flex gap-2">
              <input
                type="text"
                value={searchTerm}
                onChange={(event) => setSearchTerm(
event.target
.value)}
                placeholder="Ex.: benefícios fiscais, APIEX, ZEE..."
                className="w-full px-4 py-3 rounded-lg text-gray-900"
              />
              <button
                type="submit"
                className="px-5 py-3 bg-green-500 rounded-lg font-semibold"
              >
                Pesquisar
              </button>
            </form>
            <div className="mt-4 flex flex-wrap gap-2">
              {
home.searchSuggestions?.
map((item) => (
                <button
                  key={item}
                  onClick
={
() => setSearchTerm(item)}
                  className="text-sm bg-white/20 px-3 py-1 rounded-full"
                >
                  {item}
                </button>
              ))}
            </div>
          </div>
        </div>
      </section>
      <section className="max-w-7xl mx-auto px-6 py-10">
        <div className="grid md:grid-cols-4 gap-4">
          {
home.quickActions?.
map((action) => (
            <a
              key={
action.label
}
              href={action.url}
              className="bg-white p-5 rounded-xl shadow-sm border hover:shadow-md"
            >
              <h3 className="font-semibold">{
action.label
}</h3>
              <p className="text-sm text-gray-500 mt-2">
                Aceder agora
              </p>
            </a>
          ))}
        </div>
      </section>
      <section className="max-w-7xl mx-auto px-6 py-12">
        <h2 className="text-3xl font-bold mb-8">Sectores estratégicos</h2>
        <div className="grid md:grid-cols-3 gap-6">
          {
home.sectors?.
map((sector) => (
            <article
              key={
sector.slug
}
              className="bg-white rounded-xl border p-6 shadow-sm"
            >
              <h3 className="text-xl font-semibold">{
sector.name}<
/h3>
              <p className="text-gray-600 mt-3">{
sector.description
}</p>
              <p className="mt-4 text-sm font-medium">
                {
sector.opportunitiesCount
} oportunidades
              </p>
              <a
                href={sector.url}
                className="inline-block mt-4 text-blue-700 font-semibold"
              >
                Ver sector
              </a>
            </article>
          ))}
        </div>
      </section>
      <section className="bg-white py-12">
        <div className="max-w-7xl mx-auto px-6">
          <h2 className="text-3xl font-bold mb-8">
            Oportunidades em destaque
          </h2>
          <div className="grid md:grid-cols-3 gap-6">
            {
home.featuredOpportunities?.
map((opportunity) => (
              <article
                key={
opportunity.reference
}
                className="border rounded-xl p-6 shadow-sm"
              >
                <span className="text-sm text-blue-700 font-semibold">
                  {
opportunity.reference
}
                </span>
                <h3 className="text-xl font-bold mt-2">
                  {
opportunity.title
}
                </h3>
                <p className="text-gray-600 mt-2">{
opportunity.impact
}</p>
                <div className="mt-4 text-sm text-gray-700 space-y-1">
                  <p><b>Sector:</b> {
opportunity.sector
}</p>
                  <p><b>Província:</b> {
opportunity.province
}</p>
                  <p><b>Maturidade:</b> {
opportunity.maturity
}</p>
                  <p>
                    <b>Investimento:</b
>{
" "}
                    {
opportunity.estimatedInvestment?.currency}{
" "}
                    {
Number(opportunity.estimatedInvestment?.amount
 || 0
).toLocaleString
()}
                  </p>
                </div>
                <div className="mt-5 flex gap-3">
                  <a
                    href={opportunity.url}
                    className="text-blue-700 font-semibold"
                  >
                    Ver ficha
                  </a>
                  <a
                    href={`/manifestar-interesse?opportunity=${
opportunity.reference
}`}
                    className="text-green-700 font-semibold"
                  >
                    Manifestar interesse
                  </a>
                </div>
              </article>
            ))}
          </div>
        </div>
      </section>
      <section className="max-w-7xl mx-auto px-6 py-12">
        <h2 className="text-3xl font-bold mb-8">Como investir</h2>
        <div className="grid md:grid-cols-3 gap-6">
          {
home.investorJourney?.
map((step) => (
            <div key={
step.step
} className="bg-white border rounded-xl p-6">
              <div className="w-10 h-10 rounded-full bg-blue-900 text-white flex items-center justify-center font-bold">
                {
step.step
}
              </div>
              <h3 className="font-bold text-xl mt-4">{
step.title
}</h3>
              <p className="text-gray-600 mt-2">{
step.description
}</p>
            </div>
          ))}
        </div>
      </section>
      <section className="bg-blue-50 py-12">
        <div className="max-w-7xl mx-auto px-6 grid md:grid-cols-2 gap-8">
          <div>
            <h2 className="text-3xl font-bold mb-6">
              Incentivos e recursos legais
            </h2>
            <div className="space-y-4">
              {
home.incentivesPreview?.
map((item) => (
                <a
                  key={
item.title
}
                  href={item.url}
                  className="block bg-white border rounded-xl p-5"
                >
                  <h3 className="font-semibold">{
item.title
}</h3>
                  <p className="text-gray-600 mt-2">{
item.description
}</p>
                </a>
              ))}
            </div>
          </div>
          <div>
            <h2 className="text-3xl font-bold mb-6">
              Documentos em destaque
            </h2>
            <div className="space-y-4">
              {
home.legalHighlights?.
map((item) => (
                <a
                  key={
item.title
}
                  href={item.url}
                  className="block bg-white border rounded-xl p-5"
                >
                  <span className="text-sm text-gray-500">{
item.category
}</span>
                  <h3 className="font-semibold mt-1">{
item.title
}</h3>
                </a>
              ))}
            </div>
          </div>
        </div>
      </section>
      {
home.partnershipExchange
 && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="bg-blue-950 text-white rounded-2xl p-8">
            <h2 className="text-3xl font-bold">
              {
home.partnershipExchange.title
}
            </h2>
            <p className="mt-3 text-blue-100">
              {
home.partnershipExchange.description
}
            </p>
            <div className="mt-6 flex flex-wrap gap-4">
              {
home.partnershipExchange.actions?.
map((action) => (
                <a
                  key={
action.label
}
                  href={action.url}
                  className="px-5 py-3 bg-white text-blue-950 rounded-lg font-semibold"
                >
                  {
action.label
}
                </a>
              ))}
            </div>
          </div>
        </section>
      )}
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="grid md:grid-cols-4 gap-6">
          <Stat label="Oportunidades" value={
home.stats?.
opportunities} />
          <Stat label="Sectores" value={
home.stats?.
sectors} />
          <Stat label="Províncias" value={
home.stats?.
provinces} />
          <Stat label="Pedidos recebidos" value={
home.stats?.
interestRequests} />
        </div>
      </section>
      <footer className="bg-gray-900 text-white py-8">
        <div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between gap-4">
          <p>© {
new Date
(
).getFullYear
()} INVESTE Moçambique</p>
          <div className="flex gap-4 text-sm">
            <a href="/termos">Termos de uso</a>
            <a href="/privacidade">Política de privacidade</a>
            <a href="/contactos">Contactos</a>
          </div>
        </div>
      </footer>
    </main>
  );
}
function 
Stat({ label
, 
value }
) {
  return (
    <div className="bg-white border rounded-xl p-6 text-center shadow-sm">
      <p className="text-3xl font-bold text-blue-900"
>{value ??
 0}</p>
      <p className="text-gray-600 mt-2">{label}</p>
    </div>
  );
}
4. Laravel — routes/api.php
import 
{ useState
 } from "react";
import 
{ submitInterestRequest
 } from 
"../..
/services/homeService";
const initialForm = {
  fullName: "",
  email: "",
  phone: "",
  investorType: "",
  country: "Moçambique",
  sectorInterest: "",
  opportunityReference: "",
  investmentRange: "",
  message: "",
  privacyConsent: false,
};
export default function 
ManifestarInteresseForm(
) {
  const [form, setForm] = useState(initialForm);
  const [loading, setLoading] = useState(false);
  const [fieldErrors, setFieldErrors] = 
useState(
{});
  const [generalError, setGeneralError] = 
useState(
"");
  const [success, setSuccess] = useState(null);
  function updateField(event) {
    const 
{ name
, value, type, 
checked }
 = 
event.target
;
    setForm((previous) => ({
      ...previous,
      [name]: type === "checkbox
" ?
 
checked :
 value,
    }));
  }
  function 
validateForm(
) {
    const errors = {};
    if (
form.fullName.trim
(
).length
 < 3) {
      
errors.fullName
 = "Indique o nome completo.";
    }
    if 
(!form
.
email.includes
("@")) {
      
errors.email
 = "Indique um email válido.";
    }
    if 
(!form
.investorType) {
      
errors.investorType
 = "Seleccione o tipo de investidor.";
    }
    if 
(!form
.
country.trim
()) {
      
errors.country
 = "Indique o país.";
    }
    if 
(!form
.sectorInterest.trim()) {
      
errors.sectorInterest
 = "Indique o sector de interesse.";
    }
    if (
form.message
.
trim().length
 < 20) {
      
errors.message
 = "A mensagem deve ter pelo menos 20 caracteres.";
    }
    if 
(!form
.privacyConsent) {
      
errors.privacyConsent
 = "É necessário aceitar a política de privacidade.";
    }
    setFieldErrors(errors);
    return Object.keys(errors
).length
 === 0;
  }
  async function handleSubmit(event) {
    
event.preventDefault
();
    
setGeneralError(
"");
    setSuccess(null);
    if 
(!validateForm
()) return;
    try {
      setLoading(true);
      const response = await submitInterestRequest(form);
      
setSuccess(
response.data || response);
      setForm(initialForm);
      
setFieldErrors(
{});
    } catch (error) {
      if (
error.errors
) {
        setFieldErrors(
error.errors
);
      }
      
setGeneralError(
        
error.message
 || "Não foi possível submeter a manifestação de interesse."
      );
    } finally {
      setLoading(false);
    }
  }
  return (
    <section className="max-w-3xl mx-auto bg-white border rounded-2xl p-8 shadow-sm">
      <h2 className="text-2xl font-bold text-blue-950">
        Manifestar interesse
      </h2>
      <p className="mt-2 text-gray-600">
        Preencha os dados abaixo para que a equipa possa analisar o seu interesse
        e entrar em contacto.
      </p>
      {generalError && (
        <div className="mt-6 bg-red-50 text-red-700 border border-red-200 p-4 rounded-lg">
          {generalError}
        </div>
      )}
      {success && (
        <div className="mt-6 bg-green-50 text-green-700 border border-green-200 p-4 rounded-lg">
          <p className="font-semibold">
            Manifestação de interesse registada com sucesso.
          </p>
          {
success.reference
 && (
            <p className="mt-1">Referência: {
success.reference
}</p>
          )}
          {
success.nextStep
 && (
            <p className="mt-1">{
success.nextStep
}</p>
          )}
        </div>
      )}
      <form onSubmit={handleSubmit} className="mt-8 space-y-5">
        <Input
          label="Nome completo"
          name="fullName"
          value={
form.fullName
}
          onChange={updateField}
          error
={
fieldErrors.fullName}
          required
        />
        <Input
          label="Email"
          name="email"
          type="email"
          value={
form.email
}
          onChange={updateField}
          error
={
fieldErrors.email}
          required
        />
        <Input
          label="Telefone"
          name="phone"
          value={
form.phone
}
          onChange={updateField}
          error
={
fieldErrors.phone}
        />
        <Select
          label="Tipo de investidor"
          name="investorType"
          value={
form.investorType
}
          onChange={updateField}
          error
={
fieldErrors.investorType}
          required
          options
={
[
            ["", "Seleccione"],
            ["individual", "Investidor individual"],
            ["empresa", "Empresa"],
            ["fundo", "Fundo de investimento"],
            ["entidade_publica", "Entidade pública"],
            ["parceiro_tecnico", "Parceiro técnico"],
            ["outro", "Outro"],
          ]}
        />
        <Input
          label="País"
          name="country"
          value={
form.country
}
          onChange={updateField}
          error
={
fieldErrors.country}
          required
        />
        <Input
          label="Sector de interesse"
          name="sectorInterest"
          value={
form.sectorInterest
}
          onChange={updateField}
          error
={
fieldErrors.sectorInterest}
          required
        />
        <Input
          label="Referência da oportunidade"
          name="opportunityReference"
          value={
form.opportunityReference
}
          onChange={updateField}
          error
={
fieldErrors.opportunityReference}
          placeholder="Ex.: INV-MZ-AGR-001"
        />
        <Select
          label="Faixa de investimento"
          name="investmentRange"
          value={
form.investmentRange
}
          onChange={updateField}
          error
={
fieldErrors.investmentRange}
          options
={
[
            ["", "Seleccione"],
            [
"ate
-100000", "Até USD 100.000"],
            ["100000-500000", "USD 100.000 a USD 500.000"],
            ["500000-1000000", "USD 500.000 a USD 1.000.000"],
            ["1000000-5000000", "USD 1.000.000 a USD 5.000.000"],
            ["acima-5000000", "Acima de USD 5.000.000"],
          ]}
        />
        <div>
          <label className="block font-medium mb-1">
            Mensagem <span className="text-red-600">*</span>
          </label>
          <textarea
            name="message"
            value={
form.message
}
            onChange={updateField}
            rows="5"
            className="w-full border rounded-lg px-4 py-3"
            placeholder="Descreva o seu interesse, objectivo ou informação pretendida."
          />
          {fieldErrors.message && (
            <p className="text-sm text-red-600 mt-1">{
fieldErrors.message}<
/p>
          )}
        </div>
        <div>
          <label className="flex gap-3 items-start">
            <input
              type="checkbox"
              name="privacyConsent"
              checked={
form.privacyConsent
}
              onChange={updateField}
              className="mt-1"
            />
            <span className="text-sm text-gray-700">
              Confirmo que li e aceito a política de privacidade e autorizo o
              tratamento dos dados para resposta à minha manifestação de interesse.
            </span>
          </label>
          {fieldErrors.privacyConsent && (
            <p className="text-sm text-red-600 mt-1">
              {fieldErrors.privacyConsent}
            </p>
          )}
        </div>
        <button
          type="submit"
          disabled={loading}
          className="w-full bg-blue-950 text-white py-3 rounded-lg font-semibold disabled:opacity-60"
        >
          {
loading ?
 "A submeter...
" :
 "Submeter manifestação de interesse"}
        </button>
      </form>
    </section>
  );
}
function 
Input(
{
  label,
  name,
  value,
  onChange,
  error,
  type = "text",
  required = false,
  placeholder = "",
}) {
  return (
    <div>
      <label className="block font-medium mb-1">
        {label} {required && <span className="text-red-600">*</span>}
      </label>
      <input
        type={type}
        name={name}
        value={value}
        onChange={onChange}
        placeholder={placeholder}
        className="w-full border rounded-lg px-4 py-3"
      />
      {error && <p className="text-sm text-red-600 mt-1">{error}</p>}
    </div>
  );
}
function 
Select(
{
  label,
  name,
  value,
  onChange,
  error,
  options,
  required = false,
}) {
  return (
    <div>
      <label className="block font-medium mb-1">
        {label} {required && <span className="text-red-600">*</span>}
      </label>
      <select
        name={name}
        value={value}
        onChange={onChange}
        className="w-full border rounded-lg px-4 py-3 bg-white"
      >
        {
options.map(
([optionValue, optionLabel]) => (
          <option key={optionValue} value={optionValue}>
            {optionLabel}
          </option>
        ))}
      </select>
      {error && <p className="text-sm text-red-600 mt-1">{error}</p>}
    </div>
  );
}
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\HomeController;
use App\Http\Controllers\Api\V1\InterestRequestController;
Route::
prefix('v1')
    ->middleware(['throttle:api'])
    ->
group(
function () {
        
Route::get(
'/home', [
HomeController::
class, 'show']);
        
Route::post(
'/interest-requests', [
InterestRequestController::
class, 'store'])
            ->middleware('throttle:10,1');
    });
5. Laravel — app/Http/Controllers/Api/V1/HomeController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
class HomeController extends Controller
{
    public function 
show(
Request $request): JsonResponse
    {
        $validated = $request->
validate(
[
            'locale' => ['nullable', 'string', 
Rule::in(
['pt', 'en'])],
        ]);
        $locale = $validated['locale'
] ??
 'pt';
        $data = 
Cache::
remember("portal_home_payload_{$locale}", now
()-
>
addMinutes(
15), function () use ($locale) {
            return [
                'hero' => [
                    'title' => $locale === 'pt'
                        ? 'Invista em Moçambique com informação clara, oportunidades estruturadas e apoio especializado'
                        : 'Invest in Mozambique with clear information, structured opportunities and specialised support',
                    'subtitle' => $locale === 'pt'
                        ? 'Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas.'
                        : 'Explore strategic sectors, incentives, legal requirements and validated investment opportunities.',
                    'primaryButton' => [
                        'label' => $locale === 'pt
' ?
 'Explorar oportunidades
' :
 'Explore opportunities',
                        'url' => '/oportunidades',
                    ],
                    'secondaryButton' => [
                        'label' => $locale === 'pt
' ?
 'Como investir
' :
 'How to invest',
                        'url' => '/guia-do-investidor',
                    ],
                ],
                'quickActions' => [
                    [
                        'label' => $locale === 'pt
' ?
 'Explorar oportunidades
' :
 'Explore opportunities',
                        'url' => '/oportunidades',
                        'type' => 'primary',
                    ],
                    [
                        'label' => $locale === 'pt
' ?
 'Ver incentivos
' :
 'View incentives',
                        'url' => '/incentivos',
                        'type' => 'secondary',
                    ],
                    [
                        'label' => $locale === 'pt
' ?
 'Manifestar interesse
' :
 'Submit interest',
                        'url' => '/manifestar-interesse',
                        'type' => 'secondary',
                    ],
                    [
                        'label' => $locale === 'pt
' ?
 'Área do investidor
' :
 'Investor area',
                        'url' => '/login',
                        'type' => 'outline',
                    ],
                ],
                'searchSuggestions' => [
                    'registo na APIEX',
                    'benefícios fiscais',
                    'Zonas Económicas Especiais',
                    'repatriamento de dividendos',
                    'NUIT',
                    'licenciamento simplificado',
                ],
                'sectors' => [
                    [
                        'name' => 'Agronegócio',
                        'slug' => 'agronegocio',
                        'description' => 'Oportunidades em produção, agroprocessamento, irrigação e cadeias de valor agrícolas.',
                        'opportunitiesCount' => 12,
                        'url' => '/sectores/agronegocio',
                    ],
                    [
                        'name' => 'Energia e Mineração',
                        'slug' => 'energia-mineracao',
                        'description' => 'Projectos ligados a energias renováveis, transição energética, mineração responsável e infraestruturas associadas.',
                        'opportunitiesCount' => 9,
                        'url' => '/sectores/energia-mineracao',
                    ],
                    [
                        'name' => 'Turismo',
                        'slug' => 'turismo',
                        'description' => 'Oportunidades em turismo costeiro, cultural, ecológico e infraestruturas de apoio.',
                        'opportunitiesCount' => 7,
                        'url' => '/sectores/turismo',
                    ],
                ],
                'featuredOpportunities' => [
                    [
                        'reference' => 'INV-MZ-AGR-001',
                        'title' => 'Unidade de processamento agrícola',
                        'sector' => 'Agronegócio',
                        'province' => 'Nampula',
                        'maturity' => 'Projecto estruturado',
                        'estimatedInvestment' => [
                            'amount' => 2500000,
                            'currency' => 'USD',
                        ],
                        'impact' => 'Geração de emprego local, valorização da produção agrícola e substituição de importações.',
                        'url' => '/oportunidades/inv-mz-agr-001',
                    ],
                    [
                        'reference' => 'INV-MZ-ENE-002',
                        'title' => 'Mini-rede solar para polo produtivo',
                        'sector' => 'Energia',
                        'province' => 'Zambézia',
                        'maturity' => 'Pré-estudo disponível',
                        'estimatedInvestment' => [
                            'amount' => 1200000,
                            'currency' => 'USD',
                        ],
                        'impact' => 'Melhoria do acesso à energia para produção local e serviços essenciais.',
                        'url' => '/oportunidades/inv-mz-ene-002',
                    ],
                ],
                'investorJourney' => [
                    [
                        'step' => 1,
                        'title' => 'Identificar oportunidade',
                        'description' => 'Pesquisar sectores, províncias, oportunidades e requisitos iniciais.',
                    ],
                    [
                        'step' => 2,
                        'title' => 'Analisar requisitos',
                        'description' => 'Consultar requisitos legais, fiscais, cambiais e sectoriais.',
                    ],
                    [
                        'step' => 3,
                        'title' => 'Manifestar interesse',
                        'description' => 'Submeter contacto inicial ou pedido de informação ao portal.',
                    ],
                ],
                'incentivesPreview' => [
                    [
                        'title' => 'Benefícios fiscais',
                        'description' => 'Informação orientadora sobre benefícios fiscais aplicáveis por sector e localização.',
                        'url' => '/incentivos/beneficios-fiscais',
                    ],
                    [
                        'title' => 'ZEE e ZFI',
                        'description' => 'Resumo dos regimes especiais aplicáveis a Zonas Económicas Especiais e Zonas Francas Industriais.',
                        'url' => '/incentivos/zee-zfi',
                    ],
                ],
                'legalHighlights' => [
                    [
                        'title' => 'Guia de investimento privado',
                        'category' => 'Investimento',
                        'url' => '/recursos/guia-investimento-privado',
                    ],
                    [
                        'title' => 'Guia de repatriamento de capitais',
                        'category' => 'Regulação cambial',
                        'url' => '/recursos/repatriamento-capitais',
                    ],
                ],
                'partnershipExchange' => [
                    'title' => 'Bolsa de Parcerias, Investidores e Capital',
                    'description' => 'Ligue-se a promotores, investidores, empresas e parceiros institucionais.',
                    'actions' => [
                        [
                            'label' => 'Procurar parceiro',
                            'url' => '/parcerias',
                        ],
                        [
                            'label' => 'Submeter projecto',
                            'url' => '/submeter-projecto',
                        ],
                    ],
                ],
                'stats' => [
                    'opportunities' => 48,
                    'sectors' => 6,
                    'provinces' => 11,
                    'interestRequests' => 120,
                    'updatedAt' => now
()-
>
toDateString(
),
                ],
            ];
        });
        return 
response(
)->
json(
[
            'success' => true,
            'locale' => $locale,
            'data' => $data,
        ]);
    }
}
6. Laravel — app/Http/Requests/StoreInterestRequestRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreInterestRequestRequest extends FormRequest
{
    public function 
authorize(
): bool
    {
        return true;
    }
    public function 
rules(
): array
    {
        return [
            'fullName' => ['required', 'string', 'min:3', 'max:150'],
            'email' => ['required', 'email:
rfc,dns
', 'max:150'],
            'phone' => ['nullable', 'string', 'max:30'],
            'investorType' => [
                'required',
                'string',
                
Rule::in(
[
                    'individual',
                    'empresa',
                    'fundo',
                    'entidade_publica',
                    'parceiro_tecnico',
                    'outro',
                ]),
            ],
            'country' => ['required', 'string', 'max:100'],
            'sectorInterest' => ['required', 'string', 'max:150'],
            'opportunityReference' => ['nullable', 'string', 'max:80'],
            'investmentRange' => ['nullable', 'string', 'max:80'],
            'message' => ['required', 'string', 'min:20', 'max:2000'],
            'privacyConsent' => ['required', 'accepted'],
        ];
    }
    public function 
messages(
): array
    {
        return [
            'fullName.required' => 'O nome completo é obrigatório.',
            'fullName.min' => 'O nome completo deve ter pelo menos 3 caracteres.',
            '
email.required
' => 'O email é obrigatório.',
            '
email.email
' => 'Indique um email válido.',
            'investorType.required' => 'Seleccione o tipo de investidor.',
            'investorType.in' => 'O tipo de investidor seleccionado não é válido.',
            '
country.required
' => 'O país é obrigatório.',
            'sectorInterest.required' => 'O sector de interesse é obrigatório.',
            '
message.required
' => 'A mensagem é obrigatória.',
            'message.min' => 'A mensagem deve ter pelo menos 20 caracteres.',
            'privacyConsent.required' => 'É necessário aceitar a política de privacidade.',
            'privacyConsent.accepted' => 'É necessário aceitar a política de privacidade.',
        ];
    }
    public function 
attributes(
): array
    {
        return [
            'fullName' => 'nome completo',
            'email' => 'email',
            'phone' => 'telefone',
            'investorType' => 'tipo de investidor',
            'country' => 'país',
            'sectorInterest' => 'sector de interesse',
            'opportunityReference' => 'referência da oportunidade',
            'investmentRange' => 'faixa de investimento',
            'message' => 'mensagem',
            'privacyConsent' => 'consentimento de privacidade',
        ];
    }
}
7. Laravel — Migration create_sectors_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    public function 
up(
): void
    {
        
Schema::create(
'sectors', function (Blueprint $table) {
            $table->
id(
);
            $table->
string(
'name', 150);
            $table->
string(
'slug', 
180)-
>
unique(
);
            $table->
string(
'short_description', 
255)-
>
nullable(
);
            $table->text('description
')-
>
nullable(
);
            $table->
string(
'icon', 
100)-
>
nullable(
);
            $table->
string(
'image_path', 
255)-
>
nullable(
);
            $table->unsignedInteger('sort_order
')-
>
default(
0);
            $table->boolean('is_active
')-
>default(true);
            $table->timestamp('published_at
')-
>
nullable(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            $table->index('slug');
            $table->index('is_active');
            $table->index('sort_order');
            $table->index('published_at');
        });
    }
    public function 
down(
): void
    {
        
Schema::
dropIfExists('sectors');
    }
};
8. Laravel — Migration create_opportunities_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    public function 
up(
): void
    {
        
Schema::create(
'opportunities', function (Blueprint $table) {
            $table->
id(
);
            $table->foreignId('sector_id')
                ->constrained('sectors')
                ->
cascadeOnUpdate(
)
                ->
restrictOnDelete(
);
            $table->
string(
'reference', 
80)-
>
unique(
);
            $table->
string(
'title', 200);
            $table->
string(
'slug', 
220)-
>
unique(
);
            $table->
string(
'summary', 
500)-
>
nullable(
);
            $table->longText('description
')-
>
nullable(
);
            $table->
string(
'province', 
100)-
>
nullable(
);
            $table->
string(
'district', 
100)-
>
nullable(
);
            $table->
string(
'promoter_name', 
200)-
>
nullable(
);
            $table->
string(
'promoter_type', 
100)-
>
nullable(
);
            $table->unsignedTinyInteger('maturity_level
')-
>
nullable(
);
            $table->
string(
'maturity_label', 
120)-
>
nullable(
);
            $table->
decimal(
'estimated_investment_amount', 18, 
2)-
>
nullable(
);
            $table->
string(
'estimated_investment_currency', 
10)-
>default('USD');
            $table->
string(
'financing_modality', 
120)-
>
nullable(
);
            $table->text('expected_impact
')-
>
nullable(
);
            $table->
string(
'status', 
50)-
>default('draft');
            $table->boolean('is_featured
')-
>default(false);
            $table->timestamp('published_at
')-
>
nullable(
);
            $table->timestamp('last_validated_at
')-
>
nullable(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            $table->index('sector_id');
            $table->index('reference');
            $table->index('slug');
            $table->index('status');
            $table->index('is_featured');
            $table->index('province');
            $table->index('maturity_level');
            $table->index('published_at');
        });
    }
    public function 
down(
): void
    {
        
Schema::
dropIfExists('opportunities');
    }
};
9. Laravel — Migration create_interest_requests_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    public function 
up(
): void
    {
        
Schema::create(
'interest_requests', function (Blueprint $table) {
            $table->
id(
);
            $table->foreignId('opportunity_id')
                ->
nullable(
)
                ->constrained('opportunities')
                ->
cascadeOnUpdate(
)
                ->
nullOnDelete(
);
            $table->
string(
'reference', 
80)-
>
unique(
);
            $table->
string(
'full_name', 150);
            $table->
string(
'email', 150);
            $table->
string(
'phone', 
30)-
>
nullable(
);
            $table->
string(
'investor_type', 80);
            $table->
string(
'country', 100);
            $table->
string(
'sector_interest', 150);
            $table->
string(
'opportunity_reference', 
80)-
>
nullable(
);
            $table->
string(
'investment_range', 
80)-
>
nullable(
);
            $table->text('message');
            $table->boolean('privacy_consent
')-
>default(false);
            $table->timestamp('consented_at
')-
>
nullable(
);
            $table->
string(
'status', 
50)-
>default('received');
            $table->
string(
'source_page', 
255)-
>
nullable(
);
            $table->ipAddress('ip_address
')-
>
nullable(
);
            $table->text('user_agent
')-
>
nullable(
);
            $table->json('metadata
')-
>
nullable(
);
            $table->timestamp('reviewed_at
')-
>
nullable(
);
            $table->foreignId('reviewed_by
')-
>nullable
()-
>constrained('users
')-
>
nullOnDelete(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            $table->index('reference');
            $table->index('email');
            $table->index('investor_type');
            $table->index('sector_interest');
            $table->index('opportunity_reference');
            $table->index('status');
            $table->index('created_at');
        });
    }
    public function 
down(
): void
    {
        
Schema::
dropIfExists('interest_requests');
    }
};
10. Laravel — app/Models/InterestRequest.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InterestRequest extends Model
{
    use SoftDeletes;
    protected $fillable = [
        'opportunity_id',
        'reference',
        'full_name',
        'email',
        'phone',
        'investor_type',
        'country',
        'sector_interest',
        'opportunity_reference',
        'investment_range',
        'message',
        'privacy_consent',
        'consented_at',
        'status',
        'source_page',
        'ip_address',
        'user_agent',
        'metadata',
        'reviewed_at',
        'reviewed_by',
    ];
    protected $casts = [
        'privacy_consent' => 'boolean',
        'consented_at' => 'datetime',
        'metadata' => 'array',
        'reviewed_at' => 'datetime',
    ];
    public function 
opportunity(
): BelongsTo
    {
        return $this->
belongsTo(Opportunity::
class);
    }
    public function 
reviewer(
): BelongsTo
    {
        return $this->
belongsTo(User::
class, 'reviewed_by');
    }
}
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInterestRequestRequest;
use App\Models\InterestRequest;
use App\Models\Opportunity;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class InterestRequestController extends Controller
{
    public function 
store(
StoreInterestRequestRequest $request): JsonResponse
    {
        $validated = $request->
validated(
);
        try {
            $interestRequest = 
DB::transaction(
function () use ($request, $validated) {
                $opportunity = null;
                if 
(!empty
($validated['opportunityReference'])) {
                    $opportunity = 
Opportunity::query(
)
                        ->
where(
'reference', $validated['opportunityReference'])
                        ->
first(
);
                }
                return 
InterestRequest::create(
[
                    'opportunity_id' => $
opportunity?-
>id,
                    'reference' => $this->
generateReference(
),
                    'full_name' => trim($validated['fullName']),
                    'email' => strtolower(trim($validated['email'])),
                    'phone' => $validated['phone'] ?? null,
                    'investor_type' => $validated['investorType'],
                    'country' => $validated['country'],
                    'sector_interest' => $validated['sectorInterest'],
                    'opportunity_reference' => $validated['opportunityReference'] ?? null,
                    'investment_range' => $validated['investmentRange'] ?? null,
                    'message' => trim($validated['message']),
                    'privacy_consent' => (bool) $validated['privacyConsent'],
                    'consented_at' => 
now(
),
                    'status' => 'received',
                    'source_page' => $request->headers->get('referer'),
                    'ip_address' => $request->
ip(
),
                    'user_agent' => $request->
userAgent(
),
                    'metadata' => [
                        'locale' => $request->
input(
'locale', 'pt'),
                        'submitted_via' => 'public_home_form',
                    ],
                ]);
            });
            return 
response(
)->
json(
[
                'success' => true,
                'message' => 'Manifestação de interesse registada com sucesso.',
                'data' => [
                    'reference' => $interestRequest->reference,
                    'status' => $interestRequest->status,
                    'nextStep' => 'A equipa do portal analisará o pedido e poderá solicitar informação adicional.',
                    'submittedAt' => $interestRequest->created_
at?-
>toIso8601
String(
),
                ],
            ], 201);
        } catch (\Throwable $exception) {
            
Log::error(
'Erro ao registar manifestação de interesse', [
                'message' => $exception->
getMessage(
),
                'trace_id' => 
Str::
uuid
()-
>
toString(
),
            ]);
            return 
response(
)->
json(
[
                'success' => false,
                'message' => 'Não foi possível registar a manifestação de interesse. Tente novamente mais tarde.',
            ], 500);
        }
    }
    private function 
generateReference(
): string
    {
        $year = now
()-
>format('Y');
        $count = 
InterestRequest::withTrashed(
)
            ->
whereYear(
'created_at', $year)
            ->
count(
) + 1;
        return 
sprintf(
'IR-%s-%06d', $year, $count);
    }
}
12. Payload JSON — GET /api/v1/home
{
  "success": true,
  "locale": "pt",
  "data": {
    "hero": {
      "title": "Invista em Moçambique com informação clara, oportunidades estruturadas e apoio especializado",
      "subtitle": "Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas.",
      "primaryButton": {
        "label": "Explorar oportunidades",
        "url": "/oportunidades"
      },
      "secondaryButton": {
        "label": "Como investir",
        "url": "/guia-do-investidor"
      }
    },
    "quickActions": [
      {
        "label": "Explorar oportunidades",
        "url": "/oportunidades",
        "type": "primary"
      },
      {
        "label": "Ver incentivos",
        "url": "/incentivos",
        "type": "secondary"
      },
      {
        "label": "Manifestar interesse",
        "url": "/manifestar-interesse",
        "type": "secondary"
      },
      {
        "label": "Área do investidor",
        "url": "/login",
        "type": "outline"
      }
    ],
    "searchSuggestions": [
      "registo na APIEX",
      "benefícios fiscais",
      "Zonas Económicas Especiais",
      "repatriamento de dividendos",
      "NUIT",
      "licenciamento simplificado"
    ],
    "sectors": [
      {
        "name": "Agronegócio",
        "slug": "agronegocio",
        "description": "Oportunidades em produção, agroprocessamento, irrigação e cadeias de valor agrícolas.",
        "opportunitiesCount": 12,
        "url": "/sectores/agronegocio"
      }
    ],
    "featuredOpportunities": [
      {
        "reference": "INV-MZ-AGR-001",
        "title": "Unidade de processamento agrícola",
        "sector": "Agronegócio",
        "province": "Nampula",
        "maturity": "Projecto estruturado",
        "estimatedInvestment": {
          "amount": 2500000,
          "currency": "USD"
        },
        "impact": "Geração de emprego local, valorização da produção agrícola e substituição de importações.",
        "url": "/oportunidades/inv-mz-agr-001"
      }
    ],
    "stats": {
      "opportunities": 48,
      "sectors": 6,
      "provinces": 11,
      "interestRequests": 120,
      "updatedAt": "2026-08-27"
    }
  }
}
``
{
  "fullName": "Nome do Investidor",
  "email": "investidor@empresa.com",
  "phone": "+258 84 000 0000",
  "investorType": "empresa",
  "country": "Moçambique",
  "sectorInterest": "Agronegócio",
  "opportunityReference": "INV-MZ-AGR-001",
  "investmentRange": "1000000-5000000",
  "message": "Pretendo receber mais informação sobre a oportunidade e os requisitos aplicáveis.",
  "privacyConsent": true
}
1. Migration — create_sectors_table.php
<?php
use\
Schema;use
 Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
    public function 
up(
): void
    {
        
Schema::create(
'sectors', function (Blueprint $table) {
            $table->
id(
);
            $table->
string(
'name', 150);
            $table->
string(
'slug', 
180)-
>
unique(
);
            $table->
string(
'short_description', 
255)-
>
nullable(
);
            $table->text('description
')-
>
nullable(
);
            $table->
string(
'icon', 
100)-
>
nullable(
);
            $table->
string(
'image_path', 
255)-
>
nullable(
);
            $table->unsignedInteger('sort_order
')-
>
default(
0);
            $table->boolean('is_active
')-
>default(true);
            $table->timestamp('published_at
')-
>
nullable(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            $table->index('slug');
            $table->index('is_active');
            $table->index('sort_order');
            $table->index('published_at');
        });
    }
    public function 
down(
): void
    {
        
Schema::
dropIfExists('sectors');
    }
};
use Illuminate\Database\Schema\Blueprint;
2. Migration — create_opportunities_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    public function 
up(
): void
    {
        
Schema::create(
'opportunities', function (Blueprint $table) {
            $table->
id(
);
            $table->foreignId('sector_id')
                ->constrained('sectors')
                ->
cascadeOnUpdate(
)
                ->
restrictOnDelete(
);
            $table->
string(
'reference', 
80)-
>
unique(
);
            $table->
string(
'title', 200);
            $table->
string(
'slug', 
220)-
>
unique(
);
            $table->
string(
'summary', 
500)-
>
nullable(
);
            $table->longText('description
')-
>
nullable(
);
            $table->
string(
'province', 
100)-
>
nullable(
);
            $table->
string(
'district', 
100)-
>
nullable(
);
            $table->
string(
'promoter_name', 
200)-
>
nullable(
);
            $table->
string(
'promoter_type', 
100)-
>
nullable(
);
            $table->unsignedTinyInteger('maturity_level
')-
>
nullable(
);
            $table->
string(
'maturity_label', 
120)-
>
nullable(
);
            $table->
decimal(
'estimated_investment_amount', 18, 
2)-
>
nullable(
);
            $table->
string(
'estimated_investment_currency', 
10)-
>default('USD');
            $table->
string(
'financing_modality', 
120)-
>
nullable(
);
            $table->text('expected_impact
')-
>
nullable(
);
            $table->
string(
'status', 
50)-
>default('draft');
            $table->boolean('is_featured
')-
>default(false);
            $table->timestamp('published_at
')-
>
nullable(
);
            $table->timestamp('last_validated_at
')-
>
nullable(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            $table->index('sector_id');
            $table->index('reference');
            $table->index('slug');
            $table->index('status');
            $table->index('is_featured');
            $table->index('province');
            $table->index('maturity_level');
            $table->index('published_at');
        });
    }
    public function 
down(
): void
    {
        
Schema::
dropIfExists('opportunities');
    }
};
3. Model — app/Models/Sector.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Sector extends Model
{
    use SoftDeletes;
    protected $fillable = [
        'name',
        'slug',
        'short_description',
        'description',
        'icon',
        'image_path',
        'sort_order',
        'is_active',
        'published_at',
    ];
    protected $casts = [
        'is_active' => 'boolean',
        'published_at' => 'datetime',
    ];
    public function 
opportunities(
): HasMany
    {
        return $this->
hasMany(Opportunity::
class);
    }
}
4. Model — app/Models/Opportunity.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Opportunity extends Model
{
    use SoftDeletes;
    protected $fillable = [
        'sector_id',
        'reference',
        'title',
        'slug',
        'summary',
        'description',
        'province',
        'district',
        'promoter_name',
        'promoter_type',
        'maturity_level',
        'maturity_label',
        'estimated_investment_amount',
        'estimated_investment_currency',
        'financing_modality',
        'expected_impact',
        'status',
        'is_featured',
        'published_at',
        'last_validated_at',
    ];
    protected $casts = [
        'is_featured' => 'boolean',
        'published_at' => 'datetime',
        'last_validated_at' => 'datetime',
        'estimated_investment_amount' => 'decimal:2',
    ];
    public function 
sector(
): BelongsTo
    {
        return $this->
belongsTo(Sector::
class);
    }
}
5. Controller — app/Http/Controllers/Api/V1/SectorController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Sector;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SectorController extends Controller
{
    public function 
index(
Request $request): JsonResponse
    {
        $sectors = 
Sector::query(
)
            ->
withCount(
[
                'opportunities as opportunities_count' => function ($query) {
                    $query->
where(
'status', 'published')
                        ->whereNotNull('published_at');
                }
            ])
            ->
where(
'is_active', true)
            ->whereNotNull('published_at')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->
get(
)
            ->
map(
function (Sector $sector) {
                return [
                    'id' => $sector->id,
                    'name' => $sector->name,
                    'slug' => $sector->slug,
                    'shortDescription' => $sector->short_description,
                    'description' => $sector->description,
                    'icon' => $sector->icon,
                    'imagePath' => $sector->image_path,
                    'opportunitiesCount' => $sector->opportunities_count,
                    'url' => "/sectores/{$sector->slug}",
                ];
            });
        return 
response(
)->
json(
[
            'success' => true,
            'data' => $sectors,
        ]);
    }
    public function 
show(
string $slug): JsonResponse
    {
        $sector = 
Sector::query(
)
            ->
with(
[
                'opportunities' => function ($query) {
                    $query->
where(
'status', 'published')
                        ->whereNotNull('published_at')
                        ->latest('published_at');
                }
            ])
            ->
where(
'slug', $slug)
            ->
where(
'is_active', true)
            ->
firstOrFail(
);
        return 
response(
)->
json(
[
            'success' => true,
            'data' => [
                'id' => $sector->id,
                'name' => $sector->name,
                'slug' => $sector->slug,
                'shortDescription' => $sector->short_description,
                'description' => $sector->description,
                'icon' => $sector->icon,
                'imagePath' => $sector->image_path,
                'opportunities' => $sector->opportunities->
map(
function ($opportunity) {
                    return [
                        'reference' => $opportunity->reference,
                        'title' => $opportunity->title,
                        'slug' => $opportunity->slug,
                        'summary' => $opportunity->summary,
                        'province' => $opportunity->province,
                        'district' => $opportunity->district,
                        'maturity' => $opportunity->maturity_label,
                        'estimatedInvestment' => [
                            'amount' => $opportunity->estimated_investment_amount,
                            'currency' => $opportunity->estimated_investment_currency,
                        ],
                        'url' => "/oportunidades/{$opportunity->slug}",
                    ];
                }),
            ],
        ]);
    }
}
6. Controller — app/Http/Controllers/Api/V1/OpportunityController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Opportunity;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class OpportunityController extends Controller
{
    public function 
featured(
Request $request): JsonResponse
    {
        $limit = min((int) $request->
query(
'limit', 6), 12);
        $opportunities = 
Opportunity::query(
)
            ->with('sector:
id,name
,slug')
            ->
where(
'status', 'published')
            ->
where(
'is_featured', true)
            ->whereNotNull('published_at')
            ->latest('published_at')
            ->limit($limit)
            ->
get(
)
            ->
map(
fn (Opportunity $opportunity) => $this->formatOpportunity($opportunity));
        return 
response(
)->
json(
[
            'success' => true,
            'data' => $opportunities,
        ]);
    }
    public function 
latest(
Request $request): JsonResponse
    {
        $limit = min((int) $request->
query(
'limit', 6), 12);
        $opportunities = 
Opportunity::query(
)
            ->with('sector:
id,name
,slug')
            ->
where(
'status', 'published')
            ->whereNotNull('published_at')
            ->latest('published_at')
            ->limit($limit)
            ->
get(
)
            ->
map(
fn (Opportunity $opportunity) => $this->formatOpportunity($opportunity));
        return 
response(
)->
json(
[
            'success' => true,
            'data' => $opportunities,
        ]);
    }
    public function 
search(
Request $request): JsonResponse
    {
        $validated = $request->
validate(
[
            'query' => ['nullable', 'string', 'max:120'],
            'sector' => ['nullable', 'string', 'max:180'],
            'province' => ['nullable', 'string', 'max:100'],
            'maturityLevel' => ['nullable', 'integer', 'min:1', 'max:6'],
            'financingModality' => ['nullable', 'string', 'max:120'],
            'sort' => ['nullable', 
Rule::in(
['latest', 'investment_asc', 'investment_desc', 'maturity_desc'])],
            'perPage' => ['nullable', 'integer', 'min:1', 'max:24'],
        ]);
        $perPage = $validated['perPage'
] ??
 12;
        $query = 
Opportunity::query(
)
            ->with('sector:
id,name
,slug')
            ->
where(
'status', 'published')
            ->whereNotNull('published_at');
        if 
(!empty
($validated['query'])) {
            $term = $validated['query'];
            $query->
where(
function ($builder) use ($term) {
                $builder->
where(
'title', 'like', "%{$
term}%
")
                    ->
orWhere(
'summary', 'like', "%{$
term}%
")
                    ->
orWhere(
'description', 'like', "%{$
term}%
")
                    ->
orWhere(
'reference', 'like', "%{$
term}%
");
            });
        }
        if 
(!empty
($validated['sector'])) {
            $sectorSlug = $validated['sector'];
            $query->
whereHas(
'sector', function ($builder) use ($sectorSlug) {
                $builder->
where(
'slug', $sectorSlug);
            });
        }
        if 
(!empty
($validated['province'])) {
            $query->
where(
'province', $validated['province']);
        }
        if 
(!empty
($validated['maturityLevel'])) {
            $query->
where(
'maturity_level', $validated['maturityLevel']);
        }
        if 
(!empty
($validated['financingModality'])) {
            $query->
where(
'financing_modality', $validated['financingModality']);
        }
        match ($validated['sort'
] ??
 'latest') {
            'investment_asc' => $query->orderBy('estimated_investment_amount'),
            'investment_desc' => $query->orderByDesc('estimated_investment_amount'),
            'maturity_desc' => $query->orderByDesc('maturity_level'),
            default => $query->latest('published_at'),
        };
        $paginated = $query->paginate($perPage);
        return 
response(
)->
json(
[
            'success' => true,
            'data' => collect($paginated->
items(
))
                ->
map(
fn (Opportunity $opportunity) => $this->formatOpportunity($opportunity)),
            'meta' => [
                'currentPage' => $paginated->
currentPage(
),
                'perPage' => $paginated->
perPage(
),
                'total' => $paginated->
total(
),
                'lastPage' => $paginated->
lastPage(
),
            ],
        ]);
    }
    public function 
show(
string $slug): JsonResponse
    {
        $opportunity = 
Opportunity::query(
)
            ->with('sector:
id,name
,slug')
            ->
where(
'slug', $slug)
            ->
where(
'status', 'published')
            ->whereNotNull('published_at')
            ->
firstOrFail(
);
        return 
response(
)->
json(
[
            'success' => true,
            'data' => $this->
formatOpportunity(
$opportunity, true),
        ]);
    }
    private function 
formatOpportunity(
Opportunity $opportunity, bool $detailed = false): array
    {
        $base = [
            'id' => $opportunity->id,
            'reference' => $opportunity->reference,
            'title' => $opportunity->title,
            'slug' => $opportunity->slug,
            'summary' => $opportunity->summary,
            'sector' => [
                'name' => $opportunity->
sector?-
>name,
                'slug' => $opportunity->
sector?-
>slug,
            ],
            'province' => $opportunity->province,
            'district' => $opportunity->district,
            'maturity' => [
                'level' => $opportunity->maturity_level,
                'label' => $opportunity->maturity_label,
            ],
            'estimatedInvestment' => [
                'amount' => $opportunity->estimated_investment_amount,
                'currency' => $opportunity->estimated_investment_currency,
            ],
            'financingModality' => $opportunity->financing_modality,
            'expectedImpact' => $opportunity->expected_impact,
            'isFeatured' => $opportunity->is_featured,
            'publishedAt' => $opportunity->published_
at?-
>
toDateString(
),
            'url' => "/oportunidades/{$opportunity->slug}",
        ];
        if ($detailed) {
            $base['description'] = $opportunity->description;
            $base['promoter'] = [
                'name' => $opportunity->promoter_name,
                'type' => $opportunity->promoter_type,
            ];
            $base['lastValidatedAt'] = $opportunity->last_validated_
at?-
>
toDateString(
);
        }
        return $base;
    }
}
7. Rotas — routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\SectorController;
use App\Http\Controllers\Api\V1\OpportunityController;
Route::
prefix('v1')
    ->middleware(['throttle:api'])
    ->
group(
function () {
        
Route::get(
'/sectors', [
SectorController::
class, 'index']);
        
Route::
get('/sectors/{slug}', [
SectorController::
class, 'show']);
        
Route::get(
'/opportunities/featured', [
OpportunityController::
class, 'featured']);
        
Route::get(
'/opportunities/latest', [
OpportunityController::
class, 'latest']);
        
Route::get(
'/opportunities/search', [
OpportunityController::
class, 'search']);
        
Route::
get('/opportunities/{slug}', [
OpportunityController::
class, 'show']);
    });
8. Seeder — database/seeders/SectorSeeder.php
<?php
namespace Database\Seeders;
use App\Models\Sector;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class SectorSeeder extends Seeder
{
    public function 
run(
): void
    {
        $sectors = [
            [
                'name' => 'Agronegócio',
                'short_description' => 'Produção, agroprocessamento, irrigação e cadeias de valor agrícolas.',
                'description' => 'Sector estratégico para segurança alimentar, exportações, substituição de importações e desenvolvimento rural.',
                'icon' => 'leaf',
                'sort_order' => 1,
            ],
            [
                'name' => 'Energia e Mineração',
                'short_description' => 'Energia renovável, transição energética, mineração e infraestruturas associadas.',
                'description' => 'Área prioritária para expansão energética, industrialização e aproveitamento sustentável dos recursos naturais.',
                'icon' => 'bolt',
                'sort_order' => 2,
            ],
            [
                'name' => 'Turismo',
                'short_description' => 'Turismo costeiro, ecológico, cultural, hoteleiro e de lazer.',
                'description' => 'Sector com potencial para atracção de investimento, emprego, receitas externas e desenvolvimento territorial.',
                'icon' => 'map',
                'sort_order' => 3,
            ],
            [
                'name' => 'Infraestruturas',
                'short_description' => 'Logística, transporte, água, saneamento, energia e equipamentos públicos.',
                'description' => 'Base para competitividade económica, conectividade territorial e melhoria dos serviços essenciais.',
                'icon' => 'building',
                'sort_order' => 4,
            ],
            [
                'name' => 'Economia Azul',
                'short_description' => 'Pesca, aquacultura, portos, logística marítima e turismo costeiro.',
                'description' => 'Aproveitamento sustentável dos recursos marítimos e costeiros de Moçambique.',
                'icon' => 'waves',
                'sort_order' => 5,
            ],
            [
                'name' => 'Mercado Financeiro',
                'short_description' => 'Financiamento, capital, seguros, fintech, bolsa e instrumentos de investimento.',
                'description' => 'Sector essencial para mobilização de capital, estruturação financeira e crescimento empresarial.',
                'icon' => 'chart',
                'sort_order' => 6,
            ],
        ];
        foreach ($sectors as $sector) {
            
Sector::updateOrCreate(
                ['slug' => 
Str::
slug($sector['name'])],
                [
                    ...$sector,
                    'slug' => 
Str::
slug($sector['name']),
                    'is_active' => true,
                    'published_at' => 
now(
),
                ]
            );
        }
    }
}
9. Seeder — database/seeders/OpportunitySeeder.php
<?php
namespace Database\Seeders;
use App\Models\Opportunity;
use App\Models\Sector;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class OpportunitySeeder extends Seeder
{
    public function 
run(
): void
    {
        $agronegocio = 
Sector::where(
'slug', 'agronegocio
')-
>
first(
);
        $energia = 
Sector::where(
'slug', 'energia-e-mineracao
')-
>
first(
);
        $turismo = 
Sector::where(
'slug', 'turismo
')-
>
first(
);
        $opportunities = [
            [
                'sector_id' => $
agronegocio?-
>id,
                'reference' => 'INV-MZ-AGR-001',
                'title' => 'Unidade de processamento agrícola em Nampula',
                'summary' => 'Projecto orientado para processamento, embalagem e valorização de produtos agrícolas locais.',
                'description' => 'A oportunidade consiste na instalação de uma unidade de processamento agrícola para agregar valor à produção local, reduzir perdas pós-colheita e melhorar o acesso a mercados internos e externos.',
                'province' => 'Nampula',
                'district' => 'Nampula',
                'promoter_name' => 'Promotor em validação',
                'promoter_type' => 'privado',
                'maturity_level' => 4,
                'maturity_label' => 'Projecto estruturado',
                'estimated_investment_amount' => 2500000,
                'estimated_investment_currency' => 'USD',
                'financing_modality' => 'capital',
                'expected_impact' => 'Geração de emprego local, substituição de importações e valorização da produção agrícola.',
                'status' => 'published',
                'is_featured' => true,
            ],
            [
                'sector_id' => $
energia?-
>id,
                'reference' => 'INV-MZ-ENE-002',
                'title' => 'Mini-rede solar para polo produtivo',
                'summary' => 'Solução de energia renovável para apoiar produção local e serviços essenciais.',
                'description' => 'Projecto de mini-rede solar destinado a reforçar o acesso à energia em zona produtiva com potencial agrícola, comercial e de serviços.',
                'province' => 'Zambézia',
                'district' => 'Mocuba',
                'promoter_name' => 'Entidade promotora em validação',
                'promoter_type' => 'PPP',
                'maturity_level' => 3,
                'maturity_label' => 'Estudo de viabilidade em curso',
                'estimated_investment_amount' => 1200000,
                'estimated_investment_currency' => 'USD',
                'financing_modality' => 'PPP',
                'expected_impact' => 'Melhoria do acesso à energia, redução de custos operacionais e aumento da produtividade local.',
                'status' => 'published',
                'is_featured' => true,
            ],
            [
                'sector_id' => $
turismo?-
>id,
                'reference' => 'INV-MZ-TUR-003',
                'title' => 'Eco-lodge costeiro com integração comunitária',
                'summary' => 'Projecto turístico sustentável com foco em alojamento, cultura local e conservação ambiental.',
                'description' => 'A oportunidade prevê o desenvolvimento de uma unidade turística costeira com participação comunitária e enfoque em sustentabilidade ambiental.',
                'province' => 'Inhambane',
                'district' => 'Vilankulo',
                'promoter_name' => 'Promotor turístico privado',
                'promoter_type' => 'privado',
                'maturity_level' => 2,
                'maturity_label' => 'Pré-estudo disponível',
                'estimated_investment_amount' => 1800000,
                'estimated_investment_currency' => 'USD',
                'financing_modality' => 'joint venture',
                'expected_impact' => 'Criação de emprego, promoção do turismo sustentável e desenvolvimento comunitário.',
                'status' => 'published',
                'is_featured' => false,
            ],
        ];
        foreach ($opportunities as $opportunity) {
            if 
(!$
opportunity['sector_id']) {
                continue;
            }
            
Opportunity::updateOrCreate(
                ['reference' => $opportunity['reference']],
                [
                    ...$opportunity,
                    'slug' => 
Str::
slug($opportunity['reference'
] .
 '-' . $opportunity['title']),
                    'published_at' => 
now(
),
                    'last_validated_at' => 
now(
),
                ]
            );
        }
    }
}
10. Actualizar DatabaseSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
    public function 
run(
): void
    {
        $this->
call(
[
            
SectorSeeder::
class,
            
OpportunitySeeder::
class,
        ]);
    }
}
11. Teste — tests/Feature/SectorApiTest.php
<?php
namespace Tests\Feature;
use App\Models\Sector;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SectorApiTest extends TestCase
{
    use RefreshDatabase;
    public function test_it_lists_published_active_
sectors(
): void
    {
        
Sector::factory(
)->
create(
[
            'name' => 'Agronegócio',
            'slug' => 'agronegocio',
            'is_active' => true,
            'published_at' => 
now(
),
        ]);
        $response = $this->getJson('/api/v1/sectors');
        $response->
assertOk(
)
            ->
assertJson(
[
                'success' => true,
            ])
            ->
assertJsonPath(
'data.
0.slug
', 'agronegocio');
    }
}
12. Teste — tests/Feature/OpportunityApiTest.php
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\Sector;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OpportunityApiTest extends TestCase
{
    use RefreshDatabase;
    public function test_it_lists_featured_
opportunities(
): void
    {
        $sector = 
Sector::factory(
)->
create(
[
            'is_active' => true,
            'published_at' => 
now(
),
        ]);
        
Opportunity::factory(
)->
create(
[
            'sector_id' => $sector->id,
            'reference' => 'INV-MZ-AGR-001',
            'status' => 'published',
            'is_featured' => true,
            'published_at' => 
now(
),
        ]);
        $response = $this->getJson('/api/v1/opportunities/featured');
        $response->
assertOk(
)
            ->
assertJson(
[
                'success' => true,
            ])
            ->
assertJsonPath(
'data.
0.reference
', 'INV-MZ-AGR-001');
    }
    public function test_it_searches_opportunities_by_
query(
): void
    {
        $sector = 
Sector::factory(
)->
create(
[
            'name' => 'Agronegócio',
            'slug' => 'agronegocio',
            'is_active' => true,
            'published_at' => 
now(
),
        ]);
        
Opportunity::factory(
)->
create(
[
            'sector_id' => $sector->id,
            'title' => 'Unidade de processamento agrícola',
            'status' => 'published',
            'published_at' => 
now(
),
        ]);
        $response = $this->getJson('/api/v1/opportunities/search?query=agrícola');
        $response->
assertOk(
)
            ->
assertJson(
[
                'success' => true,
            ]);
        $this->
assertGreaterThanOrEqual(
1, count($response->json('data')));
    }
}
13. Factory — database/factories/SectorFactory.php
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class SectorFactory extends Factory
{
    public function 
definition(
): array
    {
        $name = 
fake(
)->
randomElement(
[
            'Agronegócio',
            'Energia e Mineração',
            'Turismo',
            'Infraestruturas',
            'Economia Azul',
            'Mercado Financeiro',
        ]);
        return [
            'name' => $name,
            'slug' => 
Str::slug(
$
name .
 '-' . fake
()-
>
unique(
)->
numberBetween(
1, 9999)),
            'short_description' => fake
()-
>
sentence(
),
            'description' => fake
()-
>
paragraph(
),
            'icon' => 'chart',
            'image_path' => null,
            'sort_order' => 
fake(
)->
numberBetween(
1, 10),
            'is_active' => true,
            'published_at' => 
now(
),
        ];
    }
}
14. Factory — database/factories/OpportunityFactory.php
<?php
namespace Database\Factories;
use App\Models\Sector;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class OpportunityFactory extends Factory
{
    public function 
definition(
): array
    {
        $title = 
fake(
)->
randomElement(
[
            'Unidade de processamento agrícola',
            'Mini-rede solar para polo produtivo',
            'Eco-lodge costeiro',
            'Terminal logístico regional',
            'Centro de frio para exportação',
        ]);
        $reference = 'INV-MZ-
' .
 strtoupper(fake
()-
>
bothify(
'???-###'));
        return [
            'sector_id' => 
Sector::factory(
),
            'reference' => $reference,
            'title' => $title,
            'slug' => 
Str::slug(
$
reference .
 '-' . $title),
            'summary' => fake
()-
>
sentence(
),
            'description' => 
fake(
)->
paragraphs(
3, true),
            'province' => 
fake(
)->
randomElement(
[
                'Maputo',
                'Gaza',
                'Inhambane',
                'Sofala',
                'Manica',
                'Tete',
                'Zambézia',
                'Nampula',
                'Cabo Delgado',
                'Niassa',
            ]),
            'district' => fake
()-
>
city(
),
            'promoter_name' => fake
()-
>
company(
),
            'promoter_type' => 
fake(
)->
randomElement(
['privado', 'público', 'PPP', 'cooperativa']),
            'maturity_level' => 
fake(
)->
numberBetween(
1, 6),
            'maturity_label' => 
fake(
)->
randomElement(
[
                'Ideia identificada',
                'Pré-estudo disponível',
                'Estudo de viabilidade em curso',
                'Projecto estruturado',
                'Pronto para investimento',
                'Em negociação ou implementação',
            ]),
            'estimated_investment_amount' => 
fake(
)->
numberBetween(
100000, 10000000),
            'estimated_investment_currency' => 'USD',
            'financing_modality' => 
fake(
)->
randomElement(
[
                'capital',
                'crédito',
                'PPP',
                'joint venture',
                'leasing',
                'financiamento misto',
            ]),
            'expected_impact' => fake
()-
>
sentence(
),
            'status' => 'published',
            'is_featured' => fake
()-
>
boolean(
40),
            'published_at' => 
now(
),
            'last_validated_at' => 
now(
),
        ];
    }
}
15. Payload JSON — GET /api/v1/sectors
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Agronegócio",
      "slug": "agronegocio",
      "shortDescription": "Produção, agroprocessamento, irrigação e cadeias de valor agrícolas.",
      "description": "Sector estratégico para segurança alimentar, exportações e desenvolvimento rural.",
      "icon": "leaf",
      "imagePath": null,
      "opportunitiesCount": 12,
      "url": "/sectores/agronegocio"
    }
  ]
}
16. Payload JSON — GET /api/v1/opportunities/featured
{
  "success": true,
  "data": [
    {
      "id": 1,
      "reference": "INV-MZ-AGR-001",
      "title": "Unidade de processamento agrícola em Nampula",
      "slug": "inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula",
      "summary": "Projecto orientado para processamento, embalagem e valorização de produtos agrícolas locais.",
      "sector": {
        "name": "Agronegócio",
        "slug": "agronegocio"
      },
      "province": "Nampula",
      "district": "Nampula",
      "maturity": {
        "level": 4,
        "label": "Projecto estruturado"
      },
      "estimatedInvestment": {
        "amount": "2500000.00",
        "currency": "USD"
      },
      "financingModality": "capital",
      "expectedImpact": "Geração de emprego local, substituição de importações e valorização da produção agrícola.",
      "isFeatured": true,
      "publishedAt": "2026-08-27",
      "url": "/oportunidades/inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula"
    }
  ]
}
17. Payload JSON — GET /api/v1/opportunities/search
GET /api/v1/opportunities/search?query=energia&province=Zambézia&maturityLevel=3&perPage=12
 
Resposta 
{
  "success": true,
  "data": [
    {
      "id": 2,
      "reference": "INV-MZ-ENE-002",
      "title": "Mini-rede solar para polo produtivo",
      "slug": "inv-mz-ene-002-mini-rede-solar-para-polo-produtivo",
      "summary": "Solução de energia renovável para apoiar produção local e serviços essenciais.",
      "sector": {
        "name": "Energia e Mineração",
        "slug": "energia-e-mineracao"
      },
      "province": "Zambézia",
      "district": "Mocuba",
      "maturity": {
        "level": 3,
        "label": "Estudo de viabilidade em curso"
      },
      "estimatedInvestment": {
        "amount": "1200000.00",
        "currency": "USD"
      },
      "financingModality": "PPP",
      "expectedImpact": "Melhoria do acesso à energia, redução de custos operacionais e aumento da produtividade local.",
      "isFeatured": true,
      "publishedAt": "2026-08-27",
      "url": "/oportunidades/inv-mz-ene-002-mini-rede-solar-para-polo-produtivo"
    }
  ],
  "meta": {
    "currentPage": 1,
    "perPage": 12,
    "total": 1,
    "lastPage": 1
  }
}
18. Payload JSON — GET /api/v1/opportunities/{slug}
{
  "success": true,
  "data": {
    "id": 1,
    "reference": "INV-MZ-AGR-001",
    "title": "Unidade de processamento agrícola em Nampula",
    "slug": "inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula",
    "summary": "Projecto orientado para processamento, embalagem e valorização de produtos agrícolas locais.",
    "description": "A oportunidade consiste na instalação de uma unidade de processamento agrícola para agregar valor à produção local, reduzir perdas pós-colheita e melhorar o acesso a mercados internos e externos.",
    "sector": {
      "name": "Agronegócio",
      "slug": "agronegocio"
    },
    "province": "Nampula",
    "district": "Nampula",
    "maturity": {
      "level": 4,
      "label": "Projecto estruturado"
    },
    "estimatedInvestment": {
      "amount": "2500000.00",
      "currency": "USD"
    },
    "financingModality": "capital",
    "expectedImpact": "Geração de emprego local, substituição de importações e valorização da produção agrícola.",
    "isFeatured": true,
    "publishedAt": "2026-08-27",
    "url": "/oportunidades/inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula",
    "promoter": {
      "name": "Promotor em validação",
      "type": "privado"
    },
    "lastValidatedAt": "2026-08-27"
  }
}
19. Comandos úteis
php artisan make:model Sector -m
php artisan make:model Opportunity -m
php artisan make:controller Api/V1/SectorController
php artisan make:controller Api/V1/OpportunityController
php artisan make:seeder SectorSeeder
php artisan make:seeder OpportunitySeeder
php artisan make:factory SectorFactory --model=Sector
php artisan make:factory OpportunityFactory --model=Opportunity
php artisan make:test SectorApiTest
php artisan make:test OpportunityApiTest
php artisan migrate:fresh --seed
php artisan test
Lista de sectores 
1. src/services/opportunityService.js
import axios from "axios";
const API_BASE_URL = 
import.meta.env.VITE
_API_URL || "http://localhost:8000";
const apiClient = 
axios.create
({
  baseURL: `${API_BASE_
URL}/
api/v1`,
  timeout: 15000,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    const message =
      error
?.response?.data?.
message ||
      
error?.
message ||
      "Ocorreu um erro ao comunicar com a API.";
    return Promise.reject({
      message,
      status: error
?.response?.status
 || 500,
      errors: error
?.response?.data?.
errors || null,
    });
  }
);
export async function 
getSectors(
params = {}) {
  return 
apiClient.get(
"/sectors", {
    params,
  });
}
export async function getSectorBySlug(slug) {
  return apiClient.get(`/sectors/${slug}`);
}
export async function 
getFeaturedOpportunities(
params = {}) {
  return 
apiClient.get(
"/opportunities/featured", {
    params,
  });
}
export async function 
getLatestOpportunities(
params = {}) {
  return 
apiClient.get(
"/opportunities/latest", {
    params,
  });
}
export async function 
searchOpportunities(
params = {}) {
  return 
apiClient.get(
"/opportunities/search", {
    params,
  });
}
export async function getOpportunityBySlug(slug) {
  return apiClient.get(`/opportunities/${slug}`);
}
export default {
  getSectors,
  getSectorBySlug,
  getFeaturedOpportunities,
  getLatestOpportunities,
  searchOpportunities,
  getOpportunityBySlug,
};
2. src/components/sectors/SectorCard.jsx
export default function 
SectorCard({ sector
 }) {
  if 
(!sector
) return null;
  return (
    <article className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm hover:shadow-md transition">
      <div className="flex items-start justify-between gap-4">
        <div>
          {
sector.icon
 && (
            <div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-900 flex items-center justify-center font-bold mb-4">
              {
sector.icon
}
            </div>
          )}
          <h3 className="text-xl font-bold text-blue-950">
            {sector.name}
          </h3>
          <p className="mt-3 text-gray-600 leading-relaxed">
            {
sector.shortDescription
 || 
sector.description
}
          </p>
        </div>
      </div>
      <div className="mt-5 flex items-center justify-between gap-4">
        <span className="text-sm text-gray-500">
          {
sector.opportunitiesCount
 ?? 0} oportunidades
        </span>
        <a
          href
={
sector.url || `/sectores/${
sector.slug
}`}
          className="text-sm font-semibold text-blue-800 hover:text-blue-950"
        >
          Ver sector
        </a>
      </div>
    </article>
  );
}
3. src/components/sectors/SectorGrid.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getSectors
 } from 
"../..
/services/opportunityService";
import SectorCard from 
"./
SectorCard";
export default function 
SectorGrid(
{
  title = "Sectores estratégicos",
  subtitle = "Explore áreas prioritárias de investimento em Moçambique.",
  initialSectors = null,
}) {
  const [sectors, setSectors] = 
useState(
initialSectors || []);
  const [loading, setLoading] = useState
(!initialSectors
);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    if (initialSectors) return;
    let active = true;
    async function 
loadSectors(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await 
getSectors(
);
        if (active) {
          
setSectors(
response.data || []);
        }
      } catch (err) {
        if (active) {
          
setError(err.message
 || "Não foi possível carregar os sectores.");
        }
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadSectors(
);
    return () => {
      active = false;
    };
  }, [initialSectors]);
  if (loading) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <p className="text-gray-600">A carregar sectores...</p>
      </section>
    );
  }
  if (error) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded-xl">
          {error}
        </div>
      </section>
    );
  }
  if 
(!sectors
.length) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <h2 className="text-3xl font-bold text-blue-950">{title}</h2>
        <p className="mt-3 text-gray-600">
          Ainda não existem sectores publicados.
        </p>
      </section>
    );
  }
  return (
    <section className="max-w-7xl mx-auto px-6 py-12">
      <div className="mb-8">
        <h2 className="text-3xl font-bold text-blue-950">{title}</h2>
        {subtitle && (
          <p className="mt-3 text-gray-600 max-w-3xl">
            {subtitle}
          </p>
        )}
      </div>
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        {sectors.map((sector) => (
          <SectorCard key
={sector.slug
 || sector.id} sector={sector} />
        ))}
      </div>
    </section>
  );
}
4. src/components/opportunities/OpportunityCard.jsx
export default function 
OpportunityCard({ opportunity
 }) {
  if 
(!opportunity
) return null;
  const investmentAmount = 
opportunity.estimatedInvestment?.
amount;
  const currency = 
opportunity.estimatedInvestment?.
currency || "USD";
  return (
    <article className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm hover:shadow-md transition">
      <div className="flex items-start justify-between gap-4">
        <span className="text-xs font-semibold uppercase tracking-wide text-blue-700 bg-blue-50 px-3 py-1 rounded-full">
          {
opportunity.reference
}
        </span>
        {
opportunity.isFeatured
 && (
          <span className="text-xs font-semibold text-amber-700 bg-amber-50 px-3 py-1 rounded-full">
            Destaque
          </span>
        )}
      </div>
      <h3 className="mt-4 text-xl font-bold text-blue-950">
        {
opportunity.title
}
      </h3>
      <p className="mt-3 text-gray-600 leading-relaxed">
        {
opportunity.summary
 || 
opportunity.expectedImpact
}
      </p>
      <dl className="mt-5 grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
        <div>
          <dt className="text-gray-500">Sector</dt>
          <dd className="font-medium text-gray-900">
            {typeof 
opportunity.sector
 === "string"
              ? 
opportunity.sector
              : opportunity.sector?.name}
          </dd>
        </div>
        <div>
          <dt className="text-gray-500">Província</dt>
          <dd className="font-medium text-gray-900">
            {
opportunity.province
 || "Em validação"}
          </dd>
        </div>
        <div>
          <dt className="text-gray-500">Maturidade</dt>
          <dd className="font-medium text-gray-900">
            {typeof 
opportunity.maturity
 === "string"
              ? 
opportunity.maturity
              : 
opportunity.maturity?.
label || "Em validação"}
          </dd>
        </div>
        <div>
          <dt className="text-gray-500">Investimento</dt>
          <dd className="font-medium text-gray-900">
            {investmentAmount
              ? `${currency} ${Number(investmentAmount
).toLocaleString
(
)}`
              : "Em avaliação"}
          </dd>
        </div>
      </dl>
      {
opportunity.expectedImpact
 && (
        <div className="mt-5 bg-gray-50 border border-gray-100 rounded-xl p-4">
          <p className="text-sm text-gray-600">
            <strong>Impacto esperado:</strong> {
opportunity.expectedImpact
}
          </p>
        </div>
      )}
      <div className="mt-6 flex flex-wrap gap-3">
        <a
          href
={
opportunity.url || `/oportunidades/${
opportunity.slug
}`}
          className="inline-flex items-center justify-center px-4 py-2 rounded-lg bg-blue-950 text-white text-sm font-semibold hover:bg-blue-900"
        >
          Ver ficha
        </a>
        <a
          href
={
`/manifestar-interesse?opportunity=${
encodeURIComponent(
            
opportunity.reference
 || ""
          )}`}
          className="inline-flex items-center justify-center px-4 py-2 rounded-lg border border-blue-900 text-blue-900 text-sm font-semibold hover:bg-blue-50"
        >
          Manifestar interesse
        </a>
      </div>
    </article>
  );
}
5. src/components/opportunities/FeaturedOpportunities.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getFeaturedOpportunities
 } from 
"../..
/services/opportunityService";
import OpportunityCard from 
"./
OpportunityCard";
export default function 
FeaturedOpportunities(
{
  title = "Oportunidades em destaque",
  subtitle = "Conheça projectos prioritários, estruturados ou recentemente actualizados.",
  initialOpportunities = null,
  limit = 6,
}) {
  const [opportunities, setOpportunities] = 
useState(
    initialOpportunities || []
  );
  const [loading, setLoading] = useState
(!initialOpportunities
);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    if (initialOpportunities) return;
    let active = true;
    async function 
loadOpportunities(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await 
getFeaturedOpportunities({ limit
 });
        if (active) {
          
setOpportunities(
response.data || []);
        }
      } catch (err) {
        if (active) {
          
setError(
            
err.message
 || "Não foi possível carregar as oportunidades em destaque."
          );
        }
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadOpportunities(
);
    return () => {
      active = false;
    };
  }, [initialOpportunities, limit]);
  if (loading) {
    return (
      <section className="bg-white py-12">
        <div className="max-w-7xl mx-auto px-6">
          <p className="text-gray-600">
            A carregar oportunidades em destaque...
          </p>
        </div>
      </section>
    );
  }
  if (error) {
    return (
      <section className="bg-white py-12">
        <div className="max-w-7xl mx-auto px-6">
          <div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded-xl">
            {error}
          </div>
        </div>
      </section>
    );
  }
  if 
(!opportunities
.length) {
    return (
      <section className="bg-white py-12">
        <div className="max-w-7xl mx-auto px-6">
          <h2 className="text-3xl font-bold text-blue-950">{title}</h2>
          <p className="mt-3 text-gray-600">
            Ainda não existem oportunidades em destaque publicadas.
          </p>
        </div>
      </section>
    );
  }
  return (
    <section className="bg-white py-12">
      <div className="max-w-7xl mx-auto px-6">
        <div className="mb-8 flex flex-col md:flex-row md:items-end md:justify-between gap-4">
          <div>
            <h2 className="text-3xl font-bold text-blue-950">
              {title}
            </h2>
            {subtitle && (
              <p className="mt-3 text-gray-600 max-w-3xl">
                {subtitle}
              </p>
            )}
          </div>
          <a
            href="/oportunidades"
            className="inline-flex items-center text-blue-800 font-semibold hover:text-blue-950"
          >
            Ver todas as oportunidades
          </a>
        </div>
        <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
          {opportunities.map((opportunity) => (
            <OpportunityCard
              key
={opportunity.reference
 || opportunity.id}
              opportunity={opportunity}
            />
          ))}
        </div>
      </div>
    </section>
  );
}
6. Exemplo de uso na HomePage.jsx
import SectorGrid from 
"..
/components/sectors/SectorGrid";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
export default function 
HomePage(
) {
  return (
    <main className="min-h-screen bg-gray-50">
      <section className="bg-blue-950 text-white py-20">
        <div className="max-w-7xl mx-auto px-6">
          <h1 className="text-4xl md:text-5xl font-bold">
            Invista em Moçambique com informação clara e oportunidades estruturadas
          </h1>
          <p className="mt-6 text-lg text-blue-100 max-w-3xl">
            Explore sectores estratégicos, oportunidades de investimento,
            incentivos e requisitos legais para iniciar ou expandir o seu projecto.
          </p>
          <div className="mt-8 flex flex-wrap gap-4">
            <a
              href="/oportunidades"
              className="px-6 py-3 bg-white text-blue-950 rounded-lg font-semibold"
            >
              Explorar oportunidades
            </a>
            <a
              href="/guia-do-investidor"
              className="px-6 py-3 border border-white text-white rounded-lg font-semibold"
            >
              Como investir
            </a>
          </div>
        </div>
      </section>
      <SectorGrid />
      <FeaturedOpportunities />
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="bg-blue-950 text-white rounded-2xl p-8">
          <h2 className="text-3xl font-bold">
            Pronto para manifestar interesse?
          </h2>
          <p className="mt-3 text-blue-100 max-w-3xl">
            Submeta os seus dados e indique o sector ou oportunidade de interesse.
            A equipa responsável poderá analisar o pedido e orientar os próximos passos.
          </p>
          <a
            href="/manifestar-interesse"
            className="mt-6 inline-flex px-6 py-3 bg-white text-blue-950 rounded-lg font-semibold"
          >
            Manifestar interesse
          </a>
        </div>
      </section>
    </main>
  );
}
7. Exemplo de payload esperado — GET /api/v1/sectors
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Agronegócio",
      "slug": "agronegocio",
      "shortDescription": "Produção, agroprocessamento, irrigação e cadeias de valor agrícolas.",
      "description": "Sector estratégico para segurança alimentar, exportações e desenvolvimento rural.",
      "icon": "leaf",
      "imagePath": null,
      "opportunitiesCount": 12,
      "url": "/sectores/agronegocio"
    },
    {
      "id": 2,
      "name": "Energia e Mineração",
      "slug": "energia-e-mineracao",
      "shortDescription": "Energia renovável, mineração responsável e infraestruturas associadas.",
      "description": "Área prioritária para transição energética, industrialização e aproveitamento sustentável dos recursos naturais.",
      "icon": "bolt",
      "imagePath": null,
      "opportunitiesCount": 9,
      "url": "/sectores/energia-e-mineracao"
    }
  ]
}
8. Exemplo de payload esperado — GET /api/v1/opportunities/featured
{
  "success": true,
  "data": [
    {
      "id": 1,
      "reference": "INV-MZ-AGR-001",
      "title": "Unidade de processamento agrícola em Nampula",
      "slug": "inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula",
      "summary": "Projecto orientado para processamento, embalagem e valorização de produtos agrícolas locais.",
      "sector": {
        "name": "Agronegócio",
        "slug": "agronegocio"
      },
      "province": "Nampula",
      "district": "Nampula",
      "maturity": {
        "level": 4,
        "label": "Projecto estruturado"
      },
      "estimatedInvestment": {
        "amount": "2500000.00",
        "currency": "USD"
      },
      "financingModality": "capital",
      "expectedImpact": "Geração de emprego local, substituição de importações e valorização da produção agrícola.",
      "isFeatured": true,
      "publishedAt": "2026-08-27",
      "url": "/oportunidades/inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula"
    }
  ]
}
9. Estrutura de pastas recomendada no React
src/
├
──
 components/
│   
├
──
 sectors/
│   │   
├
──
 SectorCard.jsx
│   │   └── SectorGrid.jsx
│   
├
──
 opportunities/
│   │   
├
──
 OpportunityCard.jsx
│   │   └── FeaturedOpportunities.jsx
│   └── forms/
│       └── ManifestarInteresseForm.jsx
├
──
 pages/
│   └── HomePage.jsx
├
──
 services/
│   
├
──
 homeService.js
│   └── opportunityService.js
├
──
 routes/
├
──
 hooks/
├
──
 utils/
└── assets/
10. Dependência necessária
Se ainda não tiver Axios instalado:
npm install axios
VITE_API_URL=http://localhost:8000
Há apenas um detalhe importante: no componente SectorCard, se quiser usar ícones visuais reais em vez de texto como "leaf" ou "bolt", recomendo depois integrar uma biblioteca como 
lucide-react
 ou 
react-icons
.
1. Como integrar ícones reais no SectorCard
A forma mais prática em React é usar uma biblioteca como 
Lucide React
 ou 
React Icons
.
Opção recomendada: Lucide React
Instalação:
npm install lucide-react
// src/components/sectors/sectorIcons.js
import {
  Leaf,
  Zap,
  Hotel,
  Landmark,
  Waves,
  Truck,
  Factory,
  Banknote,
} from "lucide-react";
export const sectorIcons = {
  agronegocio: Leaf,
  energia: Zap,
  turismo: Hotel,
  infraestruturas: Truck,
  economia_azul: Waves,
  mercado_financeiro: Banknote,
  
industria
: Factory,
  default: Landmark,
};
// src/components/sectors/SectorCard.jsx
import 
{ sectorIcons
 } from 
"./
sectorIcons";
export default function 
SectorCard({ sector
 }) {
  const Icon = sectorIcons[
sector.icon
_key] || sectorIcons[
sector.slug
] || sectorIcons.default;
  return (
    <article className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-md">
      <div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-blue-50 text-blue-700">
        <Icon size
={
28} strokeWidth
={
1.8} />
      </div>
      <h3 className="mb-2 text-lg font-semibold text-slate-900">
        {sector.name}
      </h3>
      <p className="mb-4 text-sm leading-6 text-slate-600">
        {
sector.description
}
      </p>
      <div className="mb-4 text-sm text-slate-500">
        {
sector.opportunities
_
count ??
 0} oportunidades disponíveis
      </div>
      <a
        href={`/sectores/${
sector.slug
}`}
        className="text-sm font-semibold text-blue-700 hover:text-blue-900"
      >
        Ver oportunidades →
      </a>
    </article>
  );
}
{
  "name": "Agronegócio",
  "slug": "agronegocio",
  "icon_key": "agronegocio",
  "description": "Oportunidades em produção, processamento e cadeias de valor agrícolas.",
  "opportunities_count": 12
}
2. Exemplo de uso do FeaturedOpportunities na página principal
Na HomePage.jsx, o ideal é receber os dados do endpoint /api/v1/home e passar apenas a lista de oportunidades em destaque para o componente.
// src/pages/HomePage.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
import Header from 
"..
/components/layout/Header";
import HeroSection from 
"..
/components/home/HeroSection";
import SectorGrid from 
"..
/components/sectors/SectorGrid";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
import LatestOpportunities from 
"..
/components/opportunities/LatestOpportunities";
import InvestorJourney from 
"..
/components/home/InvestorJourney";
import PortalStats from 
"..
/components/home/PortalStats";
import Footer from 
"..
/components/layout/Footer";
export default function 
HomePage(
) {
  const [home, setHome] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    async function 
loadHome(
) {
      try {
        const data = await getHomeData("pt");
        
setHome(
data.data ?? data);
      } catch (err) {
        
setError(
"Não foi possível carregar a página principal.");
      } finally {
        setLoading(false);
      }
    }
    
loadHome(
);
  }, []);
  if (loading) {
    return <div className="p-8 text-center">A carregar...</div>;
  }
  if (error) {
    return <div className="p-8 text-center text-red-600">{error}</div>;
  }
  return (
    <>
      <Header />
      <main>
        <HeroSection hero={
home.hero
} />
        <SectorGrid sectors
={home.sectors
 || []} />
        <FeaturedOpportunities
          title="Oportunidades em destaque"
          description="Projectos prioritários com maior relevância estratégica para investidores."
          opportunities
={home.featured
_opportunities || []}
        />
        <LatestOpportunities
          title="Oportunidades mais recentes"
          description="Projectos recentemente publicados ou actualizados na plataforma."
          opportunities
={home.latest
_opportunities || []}
        />
        <InvestorJourney steps
={home.investor
_journey || []} />
        <PortalStats stats={
home.stats
} />
      </main>
      <Footer />
    </>
  );
}
3. Componente FeaturedOpportunities.jsx
// src/components/opportunities/FeaturedOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
FeaturedOpportunities(
{
  title = "Oportunidades em destaque",
  description,
  opportunities = [],
}) {
  if 
(!opportunities
.length) {
    return null;
  }
  return (
    <section className="bg-slate-50 py-16">
      <div className="mx-auto max-w-7xl px-4">
        <div className="mb-8 flex flex-col justify-between gap-4 md:flex-row md:items-end">
          <div>
            <h2 className="text-2xl font-bold text-slate-900 md:text-3xl">
              {title}
            </h2>
            {description && (
              <p className="mt-2 max-w-2xl text-slate-600">
                {description}
              </p>
            )}
          </div>
          <a
            href="/oportunidades"
            className="text-sm font-semibold text-blue-700 hover:text-blue-900"
          >
            Ver todas →
          </a>
        </div>
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {opportunities.map((opportunity) => (
            <OpportunityCard
              key
={
opportunity.id || 
opportunity.reference
}
              opportunity={opportunity}
            />
          ))}
        </div>
      </div>
    </section>
  );
}
4. Componente OpportunityCard.jsx
// src/components/opportunities/OpportunityCard.jsx
export default function 
OpportunityCard({ opportunity
 }) {
  return (
    <article className="flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-md">
      <div className="mb-3 flex flex-wrap gap-2">
        {
opportunity.sector
 && (
          <span className="rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">
            {
opportunity.sector
}
          </span>
        )}
        {
opportunity.maturity
 && (
          <span className="rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
            {
opportunity.maturity
}
          </span>
        )}
      </div>
      <h3 className="mb-2 text-lg font-semibold text-slate-900">
        {
opportunity.title
}
      </h3>
      <p className="mb-4 line-clamp-3 text-sm leading-6 text-slate-600">
        {
opportunity.summary
}
      </p>
      <dl className="mb-5 grid gap-2 text-sm text-slate-600">
        {
opportunity.province
 && (
          <div className="flex justify-between gap-4">
            <dt className="font-medium text-slate-700">Província</dt>
            <dd>{
opportunity.province
}</dd>
          </div>
        )}
        {
opportunity.estimated
_investment && (
          <div className="flex justify-between gap-4">
            <dt className="font-medium text-slate-700">Investimento</dt>
            <dd>{
opportunity.estimated
_
investment}<
/dd>
          </div>
        )}
        {
opportunity.financing
_type && (
          <div className="flex justify-between gap-4">
            <dt className="font-medium text-slate-700">Modalidade</dt>
            <dd>{
opportunity.financing
_
type}<
/dd>
          </div>
        )}
      </dl>
      <div className="mt-auto flex flex-wrap gap-3">
        <a
          href={`/oportunidades/${
opportunity.slug
}`}
          className="rounded-lg bg-blue-700 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-800"
        >
          Ver ficha
        </a>
        <a
          href={`/manifestar-interesse?opportunity=${
opportunity.slug
}`}
          className="rounded-lg border border-blue-700 px-4 py-2 text-sm font-semibold text-blue-700 hover:bg-blue-50"
        >
          Manifestar interesse
        </a>
      </div>
    </article>
  );
}
5. Componente para listar oportunidades mais recentes
// src/components/opportunities/LatestOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
LatestOpportunities(
{
  title = "Oportunidades mais recentes",
  description,
  opportunities = [],
}) {
  if 
(!opportunities
.length) {
    return (
      <section className="py-16">
        <div className="mx-auto max-w-7xl px-4">
          <h2 className="text-2xl font-bold text-slate-900">{title}</h2>
          <p className="mt-3 text-slate-600">
            Ainda não existem oportunidades recentes publicadas.
          </p>
        </div>
      </section>
    );
  }
  return (
    <section className="py-16">
      <div className="mx-auto max-w-7xl px-4">
        <div className="mb-8 flex flex-col justify-between gap-4 md:flex-row md:items-end">
          <div>
            <h2 className="text-2xl font-bold text-slate-900 md:text-3xl">
              {title}
            </h2>
            {description && (
              <p className="mt-2 max-w-2xl text-slate-600">
                {description}
              </p>
            )}
          </div>
          <a
            href="/oportunidades?sort=latest"
            className="text-sm font-semibold text-blue-700 hover:text-blue-900"
          >
            Ver recentes →
          </a>
        </div>
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {opportunities.map((opportunity) => (
            <OpportunityCard
              key
={
opportunity.id || 
opportunity.reference
}
              opportunity={opportunity}
            />
          ))}
        </div>
      </div>
    </section>
  );
}
6. Exemplo de payload para destaque e recentes
{
  "featured_opportunities": [
    {
      "id": 1,
      "reference": "INV-MZ-AGR-001",
      "title": "Unidade de processamento agrícola em Nampula",
      "slug": "unidade-processamento-agricola-nampula",
      "summary": "Projecto orientado para processamento de produtos agrícolas e ligação a cadeias de valor regionais.",
      "sector": "Agronegócio",
      "province": "Nampula",
      "maturity": "Projecto estruturado",
      "estimated_investment": "USD 2.500.000",
      "financing_type": "Capital e parceria técnica"
    }
  ],
  "latest_opportunities": [
    {
      "id": 2,
      "reference": "INV-MZ-ENE-002",
      "title": "Sistema solar para agroindústria",
      "slug": "sistema-solar-agroindustria",
      "summary": "Oportunidade para investimento em energia renovável aplicada a unidades produtivas.",
      "sector": "Energia",
      "province": "Manica",
      "maturity": "Pré-estudo disponível",
      "estimated_investment": "USD 850.000",
      "financing_type": "Crédito, leasing ou PPP"
    }
  ]
}
Se quiser, no próximo passo posso preparar os ficheiros completos para uma estrutura real:
SectorCard.jsx
SectorGrid.jsx
sectorIcons.js
FeaturedOpportunities.jsx
LatestOpportunities.jsx
OpportunityCard.jsx
HomePage.jsx
1. src/components/sectors/SectorCard.jsx
export default function 
SectorCard({ sector
 }) {
  if 
(!sector
) return null;
  const title = sector.name || "Sector";
  const description =
    
sector.shortDescription
 ||
    
sector.short
_description ||
    
sector.description
 ||
    "Informação sectorial em validação.";
  const opportunitiesCount =
    
sector.opportunitiesCount
 ??
    
sector.opportunities
_
count ??
    0;
  const url = sector.url || `/sectores/${
sector.slug
}`;
  return (
    <article className="group bg-white border border-gray-200 rounded-2xl p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all duration-200">
      <div className="flex items-start justify-between gap-4">
        <div>
          <div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-900 flex items-center justify-center font-bold mb-5">
            {
sector.icon
 ? (
              <span className="text-sm uppercase">{
sector.icon
}</span>
            ) : (
              <span>INV</span>
            )}
          </div>
          <h3 className="text-xl font-bold text-blue-950 group-hover:text-blue-800 transition">
            {title}
          </h3>
          <p className="mt-3 text-gray-600 leading-relaxed">
            {description}
          </p>
        </div>
      </div>
      <div className="mt-6 flex items-center justify-between gap-4">
        <span className="text-sm text-gray-500">
          {opportunitiesCount
}{
" "}
          {opportunitiesCount === 
1 ?
 "oportunidade
" :
 "oportunidades"}
        </span>
        <a
          href={url}
          className="inline-flex items-center justify-center px-4 py-2 rounded-lg bg-blue-950 text-white text-sm font-semibold hover:bg-blue-800 transition"
        >
          Ver sector
        </a>
      </div>
    </article>
  );
}
2. src/components/sectors/SectorGrid.jsx
import 
{ useEffect
, 
useState }
 from "react";
import SectorCard from 
"./
SectorCard";
import 
{ getSectors
 } from 
"../..
/services/opportunityService";
export default function 
SectorGrid(
{
  title = "Sectores estratégicos",
  subtitle = "Explore áreas prioritárias de investimento em Moçambique.",
  initialSectors = null,
  showHeader = true,
}) {
  const [sectors, setSectors] = 
useState(
initialSectors || []);
  const [loading, setLoading] = useState
(!initialSectors
);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    if (initialSectors) return;
    let active = true;
    async function 
loadSectors(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await 
getSectors(
);
        if 
(!active
) return;
        const data = response?.data || response || [];
        
setSectors(
Array.isArray(data
) ?
 
data :
 []);
      } catch (err) {
        if 
(!active
) return;
        
setError(
          
err?.
message ||
            "Não foi possível carregar os sectores estratégicos."
        );
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadSectors(
);
    return () => {
      active = false;
    };
  }, [initialSectors]);
  if (loading) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="animate-pulse">
          <div className="h-8 bg-gray-200 rounded w-64 mb-4" />
          <div className="h-4 bg-gray-200 rounded w-full max-w-2xl mb-8" />
          <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
            {[1, 2, 3
].map
((item) => (
              <div
                key={item}
                className="h-56 bg-gray-100 rounded-2xl border border-gray-200"
              />
            ))}
          </div>
        </div>
      </section>
    );
  }
  if (error) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="bg-red-50 border border-red-200 text-red-700 p-5 rounded-xl">
          <p className="font-semibold">Erro ao carregar sectores</p>
          <p className="mt-1 text-sm">{error}</p>
        </div>
      </section>
    );
  }
  if 
(!sectors
.length) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        {showHeader && (
          <div className="mb-6">
            <h2 className="text-3xl font-bold text-blue-950">
              {title}
            </h2>
            {subtitle && (
              <p className="mt-3 text-gray-600 max-w-3xl">
                {subtitle}
              </p>
            )}
          </div>
        )}
        <div className="bg-gray-50 border border-gray-200 rounded-xl p-6">
          <p className="text-gray-600">
            Ainda não existem sectores publicados.
          </p>
        </div>
      </section>
    );
  }
  return (
    <section className="max-w-7xl mx-auto px-6 py-12">
      {showHeader && (
        <div className="mb-8">
          <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
            Prioridades de investimento
          </span>
          <h2 className="mt-2 text-3xl font-bold text-blue-950">
            {title}
          </h2>
          {subtitle && (
            <p className="mt-3 text-gray-600 max-w-3xl">
              {subtitle}
            </p>
          )}
        </div>
      )}
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        {sectors.map((sector) => (
          <SectorCard
            key
={sector.slug
 || sector.id || sector.name}
            sector={sector}
          />
        ))}
      </div>
    </section>
  );
}
3. src/pages/HomePage.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
import SectorGrid from 
"..
/components/sectors/SectorGrid";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
export default function 
HomePage(
) {
  const [home, setHome] = useState(null);
  const [locale, setLocale] = useState("pt");
  const [searchTerm, setSearchTerm] = 
useState(
"");
  const [loading, setLoading] = useState(true);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    let active = true;
    async function 
loadHome(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await getHomeData(locale);
        if 
(!active
) return;
        
setHome(
response?.data || response);
      } catch (err) {
        if 
(!active
) return;
        
setError(
          
err?.
message ||
            "Não foi possível carregar os dados da página principal."
        );
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadHome(
);
    return () => {
      active = false;
    };
  }, [locale]);
  function handleSearchSubmit(event) {
    
event.preventDefault
();
    const query = searchTerm.trim();
    if 
(!query
) return;
    
window.location
.href = `/oportunidades?query=${encodeURIComponent(query
)}`
;
  }
  if (loading) {
    return (
      <main className="min-h-screen flex items-center justify-center bg-gray-50">
        <div className="text-center">
          <div className="w-12 h-12 mx-auto border-4 border-blue-200 border-t-blue-900 rounded-full animate-spin" />
          <p className="mt-4 text-gray-600">
            A carregar o Portal do Investidor...
          </p>
        </div>
      </main>
    );
  }
  if (error) {
    return (
      <main className="min-h-screen flex items-center justify-center bg-gray-50 px-6">
        <div className="max-w-lg bg-white border border-red-200 rounded-2xl p-8 shadow-sm text-center">
          <h1 className="text-2xl font-bold text-red-700">
            Erro ao carregar a página
          </h1>
          <p className="mt-3 text-gray-600">{error}</p>
          <button
            onClick
={
() => 
window.location
.
reload(
)}
            className="mt-6 px-5 py-3 rounded-lg bg-blue-950 text-white font-semibold hover:bg-blue-800"
          >
            Tentar novamente
          </button>
        </div>
      </main>
    );
  }
  if 
(!home
) return null;
  const hero = 
home.hero
 || {};
  const quickActions = 
home.quickActions
 || [];
  const searchSuggestions = 
home.searchSuggestions
 || [];
  const sectors = 
home.sectors
 || [];
  const featuredOpportunities = 
home.featuredOpportunities
 || [];
  const investorJourney = 
home.investorJourney
 || [];
  const incentivesPreview = 
home.incentivesPreview
 || [];
  const legalHighlights = 
home.legalHighlights
 || [];
  const partnershipExchange = 
home.partnershipExchange
 || null;
  const stats = 
home.stats
 || {};
  return (
    <main className="min-h-screen bg-gray-50 text-gray-900">
      <Header locale={locale} setLocale={setLocale} />
      <section className="bg-blue-950 text-white">
        <div className="max-w-7xl mx-auto px-6 py-20 grid lg:grid-cols-2 gap-12 items-center">
          <div>
            <span className="inline-flex px-4 py-2 rounded-full bg-white/10 text-blue-100 text-sm font-semibold">
              INVESTE Moçambique
            </span>
            <h1 className="mt-6 text-4xl md:text-5xl font-bold leading-tight">
              {
hero.title
 ||
                "Invista em Moçambique com informação clara e oportunidades estruturadas"}
            </h1>
            <p className="mt-6 text-lg text-blue-100 leading-relaxed max-w-2xl">
              {
hero.subtitle
 ||
                "Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas."}
            </p>
            <div className="mt-8 flex flex-wrap gap-4">
              {
hero.primaryButton
 && (
                <a
                  href={hero.primaryButton.url}
                  className="inline-flex px-6 py-3 rounded-lg bg-green-500 text-blue-950 font-bold hover:bg-green-400 transition"
                >
                  {
hero.primaryButton.label
}
                </a>
              )}
              {
hero.secondaryButton
 && (
                <a
                  href={hero.secondaryButton.url}
                  className="inline-flex px-6 py-3 rounded-lg border border-white/30 text-white font-semibold hover:bg-white/10 transition"
                >
                  {
hero.secondaryButton.label
}
                </a>
              )}
            </div>
          </div>
          <div className="bg-white/10 border border-white/10 rounded-2xl p-6 backdrop-blur">
            <h2 className="text-xl font-bold">
              Pesquisa rápida de informação
            </h2>
            <p className="mt-2 text-blue-100">
              Pesquise oportunidades, incentivos, requisitos ou temas legais.
            </p>
            <form
              onSubmit={handleSearchSubmit}
              className="mt-6 flex flex-col sm:flex-row gap-3"
            >
              <input
                type="text"
                value={searchTerm}
                onChange={(event) => setSearchTerm(
event.target
.value)}
                placeholder="Ex.: benefícios fiscais, APIEX, ZEE..."
                className="w-full px-4 py-3 rounded-lg text-gray-900 outline-none focus:ring-2 focus:ring-green-400"
              />
              <button
                type="submit"
                className="px-5 py-3 bg-green-500 text-blue-950 rounded-lg font-bold hover:bg-green-400"
              >
                Pesquisar
              </button>
            </form>
            {searchSuggestions.length > 0 && (
              <div className="mt-5 flex flex-wrap gap-2">
                {searchSuggestions.map((suggestion) => (
                  <button
                    key={suggestion}
                    type="button"
                    onClick
={
() => setSearchTerm(suggestion)}
                    className="text-sm bg-white/15 hover:bg-white/25 px-3 py-1 rounded-full"
                  >
                    {suggestion}
                  </button>
                ))}
              </div>
            )}
          </div>
        </div>
      </section>
      {quickActions.length > 0 && (
        <section className="max-w-7xl mx-auto px-6 py-10">
          <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
            {quickActions.map((action) => (
              <a
                key={
action.label
}
                href={action.url}
                className="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md hover:border-blue-200 transition"
              >
                <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                  Acesso rápido
                </span>
                <h3 className="mt-2 font-bold text-blue-950">
                  {
action.label
}
                </h3>
                <p className="mt-2 text-sm text-gray-500">
                  Aceder agora
                </p>
              </a>
            ))}
          </div>
        </section>
      )}
      <SectorGrid initialSectors={sectors} />
      <FeaturedOpportunities initialOpportunities={featuredOpportunities} />
      {investorJourney.length > 0 && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="mb-8">
            <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
              Guia do investidor
            </span>
            <h2 className="mt-2 text-3xl font-bold text-blue-950">
              Como investir em Moçambique
            </h2>
            <p className="mt-3 text-gray-600 max-w-3xl">
              Siga um percurso simples para identificar oportunidades,
              compreender requisitos e iniciar contacto.
            </p>
          </div>
          <div className="grid md:grid-cols-3 gap-6">
            {investorJourney.map((step) => (
              <div
                key={
step.step
}
                className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm"
              >
                <div className="w-11 h-11 rounded-full bg-blue-950 text-white flex items-center justify-center font-bold">
                  {
step.step
}
                </div>
                <h3 className="mt-5 text-xl font-bold text-blue-950">
                  {
step.title
}
                </h3>
                <p className="mt-3 text-gray-600 leading-relaxed">
                  {
step.description
}
                </p>
              </div>
            ))}
          </div>
        </section>
      )}
      {(incentivesPreview.length > 0 || legalHighlights.length > 0) && (
        <section className="bg-blue-50 py-12">
          <div className="max-w-7xl mx-auto px-6 grid lg:grid-cols-2 gap-8">
            <div>
              <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                Incentivos
              </span>
              <h2 className="mt-2 text-3xl font-bold text-blue-950">
                Incentivos e benefícios
              </h2>
              <div className="mt-6 space-y-4">
                {incentivesPreview.map((item) => (
                  <a
                    key={
item.title
}
                    href={item.url}
                    className="block bg-white border border-blue-100 rounded-xl p-5 hover:shadow-md transition"
                  >
                    <h3 className="font-bold text-blue-950">
                      {
item.title
}
                    </h3>
                    <p className="mt-2 text-gray-600">
                      {
item.description
}
                    </p>
                  </a>
                ))}
              </div>
            </div>
            <div>
              <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                Recursos legais
              </span>
              <h2 className="mt-2 text-3xl font-bold text-blue-950">
                Documentos em destaque
              </h2>
              <div className="mt-6 space-y-4">
                {legalHighlights.map((item) => (
                  <a
                    key={
item.title
}
                    href={item.url}
                    className="block bg-white border border-blue-100 rounded-xl p-5 hover:shadow-md transition"
                  >
                    <span className="text-sm text-gray-500">
                      {
item.category
}
                    </span>
                    <h3 className="mt-1 font-bold text-blue-950">
                      {
item.title
}
                    </h3>
                  </a>
                ))}
              </div>
            </div>
          </div>
        </section>
      )}
      {partnershipExchange && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="bg-blue-950 text-white rounded-2xl p-8 md:p-10">
            <span className="text-sm uppercase tracking-wide text-green-300 font-semibold">
              Parcerias e capital
            </span>
            <h2 className="mt-3 text-3xl font-bold">
              {partnershipExchange.title}
            </h2>
            <p className="mt-3 text-blue-100 max-w-3xl">
              {partnershipExchange.description}
            </p>
            <div className="mt-6 flex flex-wrap gap-4">
              {
partnershipExchange.actions?.
map((action) => (
                <a
                  key={
action.label
}
                  href={action.url}
                  className="inline-flex px-5 py-3 rounded-lg bg-white text-blue-950 font-semibold hover:bg-blue-50"
                >
                  {
action.label
}
                </a>
              ))}
            </div>
          </div>
        </section>
      )}
      <PortalStats stats={stats} />
      <Footer />
    </main>
  );
}
function 
Header({ locale
, 
setLocale }
) {
  return (
    <header className="bg-white border-b border-gray-200 sticky top-0 z-40">
      <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-6">
        <a href="/" className="font-black text-xl text-blue-950">
          INVESTE Moçambique
        </a>
        <nav className="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-700">
          <a href="/oportunidades" className="hover:text-blue-900">
            Oportunidades
          </a>
          <a href="/sectores" className="hover:text-blue-900">
            Sectores
          </a>
          <a href="/guia-do-investidor" className="hover:text-blue-900">
            Como investir
          </a>
          <a href="/incentivos" className="hover:text-blue-900">
            Incentivos
          </a>
          <a href="/recursos" className="hover:text-blue-900">
            Recursos legais
          </a>
        </nav>
        <div className="flex items-center gap-3">
          <button
            type="button"
            onClick
={
() => setLocale("pt")}
            className
={
              locale === "pt"
                ? "font-bold text-blue-950"
                : "text-gray-500 hover:text-blue-950"
            }
          >
            PT
          </button>
          <span className="text-gray-300">|</span>
          <button
            type="button"
            onClick
={
() => setLocale("en")}
            className
={
              locale === "en"
                ? "font-bold text-blue-950"
                : "text-gray-500 hover:text-blue-950"
            }
          >
            EN
          </button>
          <a
            href="/login"
            className="hidden sm:inline-flex px-4 py-2 rounded-lg border border-blue-950 text-blue-950 text-sm font-semibold hover:bg-blue-50"
          >
            Área do investidor
          </a>
        </div>
      </div>
    </header>
  );
}
function 
PortalStats({ stats
 }) {
  const items = [
    {
      label: "Oportunidades",
      value: 
stats?.opportunities ??
 0,
    },
    {
      label: "Sectores",
      value: 
stats?.sectors ??
 0,
    },
    {
      label: "Províncias",
      value: 
stats?.provinces ??
 0,
    },
    {
      label: "Pedidos recebidos",
      value: 
stats?.interestRequests ??
 0,
    },
  ];
  return (
    <section className="max-w-7xl mx-auto px-6 py-12">
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
        {items.map((item) => (
          <div
            key={
item.label
}
            className="bg-white border border-gray-200 rounded-2xl p-6 text-center shadow-sm"
          >
            <p className="text-3xl font-black text-blue-950">
              {Number(item.value
).toLocaleString
()}
            </p>
            <p className="mt-2 text-gray-600">{
item.label
}</p>
          </div>
        ))}
      </div>
      {
stats?.
updatedAt && (
        <p className="mt-4 text-sm text-gray-500 text-center">
          Dados actualizados em {
stats.updatedAt
}
        </p>
      )}
    </section>
  );
}
function 
Footer(
) {
  return (
    <footer className="bg-gray-950 text-white py-10">
      <div className="max-w-7xl mx-auto px-6 flex flex-col lg:flex-row justify-between gap-8">
        <div>
          <h2 className="font-black text-xl">INVESTE Moçambique</h2>
          <p className="mt-3 text-gray-300 max-w-md">
            Plataforma de informação, orientação e facilitação de oportunidades
            de investimento em Moçambique.
          </p>
        </div>
        <div className="grid sm:grid-cols-3 gap-8 text-sm">
          <div>
            <h3 className="font-bold mb-3">Portal</h3>
            <ul className="space-y-2 text-gray-300">
              <li><a href="/oportunidades">Oportunidades</a></li>
              <li><a href="/sectores">Sectores</a></li>
              <li><a href="/incentivos">Incentivos</a></li>
            </ul>
          </div>
          <div>
            <h3 className="font-bold mb-3">Apoio</h3>
            <ul className="space-y-2 text-gray-300">
              <li><a href="/guia-do-investidor">Guia do investidor</a></li>
              <li><a href="/recursos">Recursos legais</a></li>
              <li><a href="/contactos">Contactos</a></li>
            </ul>
          </div>
          <div>
            <h3 className="font-bold mb-3">Legal</h3>
            <ul className="space-y-2 text-gray-300">
              <li><a href="/termos">Termos de uso</a></li>
              <li><a href="/privacidade">Política de privacidade</a></li>
            </ul>
          </div>
        </div>
      </div>
    </footer>
  );
}
Observação importante
Esta HomePage.jsx assume que já existem:
src/services/homeService.js
src/services/opportunityService.js
src/components/opportunities/FeaturedOpportunities.jsx
1. FeaturedOpportunities.jsx
// src/components/opportunities/FeaturedOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
FeaturedOpportunities(
{
  title = "Oportunidades em destaque",
  description = "Projectos prioritários com maior relevância estratégica para investidores nacionais e estrangeiros.",
  opportunities = [],
  viewAllUrl = "/oportunidades",
}) {
  if 
(!opportunities
 || 
opportunities.length
 === 0) {
    return (
      <section className="bg-slate-50 py-16">
        <div className="mx-auto max-w-7xl px-4">
          <div className="rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center">
            <h2 className="text-2xl font-bold text-slate-900">{title}</h2>
            <p className="mt-3 text-slate-600">
              Ainda não existem oportunidades em destaque publicadas.
            </p>
          </div>
        </div>
      </section>
    );
  }
  return (
    <section className="bg-slate-50 py-16">
      <div className="mx-auto max-w-7xl px-4">
        <div className="mb-8 flex flex-col justify-between gap-4 md:flex-row md:items-end">
          <div>
            <span className="mb-2 inline-flex rounded-full bg-blue-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700">
              Carteira de projectos
            </span>
            <h2 className="text-2xl font-bold text-slate-900 md:text-3xl">
              {title}
            </h2>
            {description && (
              <p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600 md:text-base">
                {description}
              </p>
            )}
          </div>
          <a
            href={viewAllUrl}
            className="inline-flex items-center justify-center rounded-xl border border-blue-700 px-5 py-3 text-sm font-semibold text-blue-700 transition hover:bg-blue-700 hover:text-white"
          >
            Ver todas →
          </a>
        </div>
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {opportunities.map((opportunity) => (
            <OpportunityCard
              key
={
opportunity.id || 
opportunity.reference
 || 
opportunity.slug
}
              opportunity={opportunity}
            />
          ))}
        </div>
      </div>
    </section>
  );
}
2. OpportunityCard.jsx
// src/components/opportunities/OpportunityCard.jsx
function formatInvestment(value) {
  if 
(!string
") 
{  if
 
(!value
) return null;
    return value;
  }
  if (typeof value === "object" && 
value.amount
 && 
value.currency
) {
    return new Intl.NumberFormat("pt-MZ", {
      style: "currency",
      currency: 
value.currency
,
      maximumFractionDigits: 0,
    }
).format
(
value.amount
);
  }
  return null;
}
export default function 
OpportunityCard({ opportunity
 }) {
  const {
    title,
    name,
    slug,
    reference,
    summary,
    description,
    sector,
    province,
    district,
    maturity,
    estimated_investment,
    estimatedInvestment,
    financing_type,
    financingType,
    impact,
    is_featured,
    updated_at,
  } = opportunity;
  const displayTitle = title || name || "Oportunidade de investimento";
  const displaySummary =
    summary || description || "Informação resumida da oportunidade em validação.";
  const investment = 
formatInvestment(
estimated_investment || estimatedInvestment);
  const financing = financing_type || financingType;
  const opportunityUrl = 
slug ?
 `/oportunidades/${slug}
` :
 "/oportunidades";
  const interestUrl = slug
    ? `/manifestar-interesse?opportunity=${slug}`
    : "/manifestar-interesse";
  return (
    <article className="flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-md">
      <div className="mb-4 flex flex-wrap gap-2">
        {is_featured && (
          <span className="rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">
            Destaque
          </span>
        )}
        {sector && (
          <span className="rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">
            {typeof sector === "string
" ?
 
sector :
 sector.name}
          </span>
        )}
        {maturity && (
          <span className="rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
            {maturity}
          </span>
        )}
      </div>
      {reference && (
        <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
          {reference}
        </p>
      )}
      <h3 className="mb-3 text-lg font-bold leading-snug text-slate-900">
        {displayTitle}
      </h3>
      <p className="mb-5 line-clamp-3 text-sm leading-6 text-slate-600">
        {displaySummary}
      </p>
      <dl className="mb-5 space-y-3 text-sm text-slate-600">
        {(province || district) && (
          <div className="flex justify-between gap-4 border-b border-slate-100 pb-2">
            <dt className="font-medium text-slate-700">Localização</dt>
            <dd className="text-right">
              {[district, province
].filter
(Boolean
).join
(", ")}
            </dd>
          </div>
        )}
        {investment && (
          <div className="flex justify-between gap-4 border-b border-slate-100 pb-2">
            <dt className="font-medium text-slate-700">Investimento</dt>
            <dd className="text-right">{investment}</dd>
          </div>
        )}
        {financing && (
          <div className="flex justify-between gap-4 border-b border-slate-100 pb-2">
            <dt className="font-medium text-slate-700">Financiamento</dt>
            <dd className="text-right">{financing}</dd>
          </div>
        )}
        {impact && (
          <div className="flex justify-between gap-4 border-b border-slate-100 pb-2">
            <dt className="font-medium text-slate-700">Impacto</dt>
            <dd className="text-right">{impact}</dd>
          </div>
        )}
      </dl>
      {updated_at && (
        <p className="mb-4 text-xs text-slate-400">
          Actualizado 
em{
" "}
          {new Intl.DateTimeFormat("pt-MZ", {
            day: "2-digit",
            month: "long",
            year: "numeric",
          }
).format
(
new Date
(updated_at))}
        </p>
      )}
      <div className="mt-auto flex flex-wrap gap-3">
        <a
          href={opportunityUrl}
          className="inline-flex flex-1 items-center justify-center rounded-xl bg-blue-700 px-4 py-3 text-sm font-semibold text-white transition hover:bg-blue-800"
        >
          Ver ficha
        </a>
        <a
          href={interestUrl}
          className="inline-flex flex-1 items-center justify-center rounded-xl border border-blue-700 px-4 py-3 text-sm font-semibold text-blue-700 transition hover:bg-blue-50"
        >
          Manifestar interesse
        </a>
      </div>
    </article>
  );
}
3. homeService.js
// src/services/homeService.js
const API_URL = 
import.meta.env.VITE
_API_URL || "http://localhost:8000";
async function 
request(
path, options = {}) {
  const url = `${API_
URL}$
{path}`;
  const response = await 
fetch(
url, {
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      ...
options.headers
,
    },
    ...options,
  });
  let payload = null;
  try {
    payload = await 
response.json
();
  } catch {
    payload = null;
  }
  if 
(!response
.ok) {
    const message =
      
payload?.
message ||
      
payload?.
error ||
      "Não foi possível concluir o pedido à API.";
    const error = new Error(message);
    
error.status
 = 
response.status
;
    
error.payload
 = payload;
    throw error;
  }
  return payload;
}
export async function 
getHomeData(
locale = "pt") {
  const query = new 
URLSearchParams({ locale
 }
).toString
();
  const payload = await request(`/api/v1/
home?$
{query}`);
  return payload?.data || payload;
}
export async function 
getFeaturedOpportunities(
locale = "pt", limit = 6) {
  const query = new 
URLSearchParams(
{
    locale,
    limit: String(limit),
  }
).toString
();
  const payload = await request(`/api/v1/opportunities/
featured?$
{query}`);
  return payload?.data || [];
}
export async function 
getLatestOpportunities(
locale = "pt", limit = 6) {
  const query = new 
URLSearchParams(
{
    locale,
    limit: String(limit),
  }
).toString
();
  const payload = await request(`/api/v1/opportunities/
latest?$
{query}`);
  return payload?.data || [];
}
export async function 
searchOpportunities(
{
  q = "",
  sector = "",
  province = "",
  maturity = "",
  financing_type = "",
  page = 1,
  per_page = 12,
  locale = "pt",
} = {}) {
  const query = new 
URLSearchParams(
{
    locale,
    page: String(page),
    per_page: String(per_page),
  });
  if (q) 
query.append
("q", q);
  if (sector) 
query.append
("sector", sector);
  if (province) 
query.append
("province", province);
  if (maturity) 
query.append
("maturity", maturity);
  if (financing_type) 
query.append
("financing_type", financing_type);
  const payload = await request(`/api/v1/opportunities/
search?$
{query}`);
  return payload;
}
export default {
  getHomeData,
  getFeaturedOpportunities,
  getLatestOpportunities,
  searchOpportunities,
};
Exemplo rápido de uso na HomePage.jsx
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
import 
{ getHomeData
 } from 
"..
/services/homeService";
<FeaturedOpportunities
  title="Oportunidades em destaque"
  description="Projectos prioritários com maior relevância estratégica para investidores."
  opportunities
={home.featured
_opportunities || []}
/>;
1. LatestOpportunities.jsx
// src/components/opportunities/LatestOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
LatestOpportunities(
{
  title = "Oportunidades mais recentes",
  description = "Projectos recentemente publicados ou actualizados na plataforma.",
  opportunities = [],
  viewAllUrl = "/oportunidades?sort=latest",
  limit,
}) {
  const visibleOpportunities = limit
    ? 
opportunities.slice
(0, limit)
    : opportunities;
  return (
    <section className="bg-white py-16">
      <div className="mx-auto max-w-7xl px-4">
        <div className="mb-8 flex flex-col justify-between gap-4 md:flex-row md:items-end">
          <div>
            <span className="mb-2 inline-flex rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700">
              Actualizações recentes
            </span>
            <h2 className="text-2xl font-bold text-slate-900 md:text-3xl">
              {title}
            </h2>
            {description && (
              <p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600 md:text-base">
                {description}
              </p>
            )}
          </div>
          <a
            href={viewAllUrl}
            className="inline-flex items-center justify-center rounded-xl border border-slate-300 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-emerald-600 hover:text-emerald-700"
          >
            Ver recentes →
          </a>
        </div>
        {visibleOpportunities.length === 
0 ?
 (
          <div className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 p-8 text-center">
            <h3 className="text-lg font-semibold text-slate-900">
              Ainda não existem oportunidades recentes.
            </h3>
            <p className="mt-2 text-sm text-slate-600">
              Assim que novas oportunidades forem publicadas ou actualizadas,
              elas serão apresentadas nesta área.
            </p>
          </div>
        ) : (
          <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
            {visibleOpportunities.map((opportunity) => (
              <OpportunityCard
                key
={
opportunity.id || 
opportunity.reference
 || 
opportunity.slug
}
                opportunity={opportunity}
              />
            ))}
          </div>
        )}
      </div>
    </section>
  );
}
2. interestRequestService.js
// src/services/interestRequestService.js
const API_URL = 
import.meta.env.VITE
_API_URL || "http://localhost:8000";
async function 
request(
path, options = {}) {
  const response = await fetch(`${API_
URL}$
{path}`, {
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      ...
options.headers
,
    },
    ...options,
  });
  let payload = null;
  try {
    payload = await 
response.json
();
  } catch {
    payload = null;
  }
  if 
(!response
.ok) {
    const error = new 
Error(
      
payload?.
message || "Não foi possível submeter o pedido."
    );
    
error.status
 = 
response.status
;
    
error.payload
 = payload;
    
error.errors
 = 
payload?.
errors || {};
    throw error;
  }
  return payload;
}
export async function submitInterestRequest(data) {
  return 
request(
"/api/v1/interest-requests", {
    method: "POST",
    body: JSON.stringify(data),
  });
}
export function normalizeInterestRequestPayload(form) {
  return {
    name: 
form.name?.trim(
),
    email: 
form.email?.trim(
),
    phone: 
form.phone?.trim(
) || null,
    country: 
form.country?.trim(
),
    company_name: 
form.company
_
name?.trim(
) || null,
    investor_type: 
form.investor
_type,
    sector_id: 
form.sector
_id || null,
    opportunity_id: 
form.opportunity
_id || null,
    investment_range: 
form.investment
_range || null,
    message: 
form.message?.trim(
),
    privacy_consent: Boolean(
form.privacy
_consent),
    // Campo honeypot 
anti-spam
. Deve ficar vazio.
    website: 
form.website
 || "",
  };
}
export default {
  submitInterestRequest,
  normalizeInterestRequestPayload,
};
3. Exemplo de uso do OpportunityCard.jsx
Uso dentro da HomePage.jsx
// src/pages/HomePage.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
import LatestOpportunities from 
"..
/components/opportunities/LatestOpportunities";
export default function 
HomePage(
) {
  const [home, setHome] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    async function 
loadHome(
) {
      try {
        const data = await getHomeData("pt");
        setHome(data);
      } catch (err) {
        
setError(
"Não foi possível carregar a página principal.");
      } finally {
        setLoading(false);
      }
    }
    
loadHome(
);
  }, []);
  if (loading) {
    return <div className="p-8 text-center">A carregar...</div>;
  }
  if (error) {
    return <div className="p-8 text-center text-red-600">{error}</div>;
  }
  return (
    <main>
      <FeaturedOpportunities
        opportunities
={home.featured
_opportunities || []}
      />
      <LatestOpportunities
        opportunities
={home.latest
_opportunities || []}
        limit
={
6}
      />
    </main>
  );
}
4. Exemplo de uso isolado do OpportunityCard.jsx
// src/pages/OpportunityExamplePage.jsx
import OpportunityCard from 
"..
/components/opportunities/OpportunityCard";
const opportunity = {
  id: 1,
  reference: "INV-MZ-AGR-001",
  title: "Unidade de processamento agrícola em Nampula",
  slug: "unidade-processamento-agricola-nampula",
  summary:
    "Projecto orientado para processamento de produtos agrícolas e ligação a cadeias de valor regionais.",
  sector: "Agronegócio",
  province: "Nampula",
  district: "Nacala",
  maturity: "Projecto estruturado",
  estimated_investment: {
    amount: 2500000,
    currency: "USD",
  },
  financing_type: "Capital e parceria técnica",
  impact: "Emprego local e aumento da capacidade de processamento",
  is_featured: true,
  updated_at: "2026-08-20T10:30:00Z",
};
export default function 
OpportunityExamplePage(
) {
  return (
    <section className="bg-slate-50 py-16">
      <div className="mx-auto max-w-7xl px-4">
        <h1 className="mb-8 text-3xl font-bold text-slate-900">
          Exemplo de oportunidade
        </h1>
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          <OpportunityCard opportunity={opportunity} />
        </div>
      </div>
    </section>
  );
}
5. Exemplo de payload esperado do Laravel
{
  "data": {
    "featured_opportunities": [
      {
        "id": 1,
        "reference": "INV-MZ-AGR-001",
        "title": "Unidade de processamento agrícola em Nampula",
        "slug": "unidade-processamento-agricola-nampula",
        "summary": "Projecto orientado para processamento de produtos agrícolas.",
        "sector": "Agronegócio",
        "province": "Nampula",
        "district": "Nacala",
        "maturity": "Projecto estruturado",
        "estimated_investment": {
          "amount": 2500000,
          "currency": "USD"
        },
        "financing_type": "Capital e parceria técnica",
        "impact": "Emprego local e exportações",
        "is_featured": true,
        "updated_at": "2026-08-20T10:30:00Z"
      }
    ],
    "latest_opportunities": [
      {
        "id": 2,
        "reference": "INV-MZ-ENE-002",
        "title": "Sistema solar para agroindústria",
        "slug": "sistema-solar-agroindustria",
        "summary": "Energia renovável aplicada a unidades produtivas.",
        "sector": "Energia",
        "province": "Manica",
        "maturity": "Pré-estudo disponível",
        "estimated_investment": {
          "amount": 850000,
          "currency": "USD"
        },
        "financing_type": "Crédito, leasing ou PPP",
        "impact": "Redução de custos energéticos",
        "is_featured": false,
        "updated_at": "2026-08-22T09:00:00Z"
      }
    ]
  }
}
1. Migration create_interest_requests_table.php
Caminho recomendado:
database/migrations/2026_08_27_000000_create_interest_requests_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function 
up(
): void
    {
        
Schema::create(
'interest_requests', function (Blueprint $table) {
            $table->
id(
);
            $table->
string(
'reference_code', 
50)-
>
unique(
);
            // Dados do interessado
            $table->
string(
'name', 150);
            $table->
string(
'email', 180);
            $table->
string(
'phone', 
40)-
>
nullable(
);
            $table->
string(
'country', 
100)-
>
nullable(
);
            $table->
string(
'company_name', 
180)-
>
nullable(
);
            // Perfil do investidor
            $table->
string(
'investor_type', 50);
            $table->
string(
'investment_range', 
80)-
>
nullable(
);
            // Relação com sectores e oportunidades
            $table->foreignId('sector_id')
                ->
nullable(
)
                ->constrained('sectors')
                ->
nullOnDelete(
);
            $table->foreignId('opportunity_id')
                ->
nullable(
)
                ->constrained('opportunities')
                ->
nullOnDelete(
);
            // Mensagem e origem
            $table->text('message');
            $table->
string(
'source_page', 
255)-
>
nullable(
);
            // Estado do pedido
            $table->
string(
'status', 
50)-
>default('new');
            $table->timestamp('reviewed_at
')-
>
nullable(
);
            $table->foreignId('reviewed_by')
                ->
nullable(
)
                ->constrained('users')
                ->
nullOnDelete(
);
            $table->text('internal_notes
')-
>
nullable(
);
            // Consentimento e auditoria
            $table->timestamp('privacy_consent_at
')-
>
nullable(
);
            $table->
string(
'ip_address', 
64)-
>
nullable(
);
            $table->text('user_agent
')-
>
nullable(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            // Índices
            $table->index('email');
            $table->index('status');
            $table->index('investor_type');
            $table->index('sector_id');
            $table->index('opportunity_id');
            $table->index('created_at');
        });
    }
    /**
     * Reverse the migrations.
     */
    public function 
down(
): void
    {
        
Schema::
dropIfExists('interest_requests');
    }
};
2. Model InterestRequest.php
app/Models/InterestRequest.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class InterestRequest extends Model
{
    use HasFactory;
    use SoftDeletes;
    public const STATUS_NEW = 'new';
    public const STATUS_IN_REVIEW = 'in_review';
    public const STATUS_CONTACTED = 'contacted';
    public const STATUS_QUALIFIED = 'qualified';
    public const STATUS_REJECTED = 'rejected';
    public const STATUS_CLOSED = 'closed';
    public const INVESTOR_TYPE_INDIVIDUAL = 'individual';
    public const INVESTOR_TYPE_COMPANY = 'empresa';
    public const INVESTOR_TYPE_FUND = 'fundo';
    public const INVESTOR_TYPE_FINANCIAL_INSTITUTION = 'instituicao_financeira';
    public const INVESTOR_TYPE_PARTNER = 'parceiro';
    public const INVESTOR_TYPE_OTHER = 'outro';
    protected $fillable = [
        'reference_code',
        'name',
        'email',
        'phone',
        'country',
        'company_name',
        'investor_type',
        'investment_range',
        'sector_id',
        'opportunity_id',
        'message',
        'source_page',
        'status',
        'reviewed_at',
        'reviewed_by',
        'internal_notes',
        'privacy_consent_at',
        'ip_address',
        'user_agent',
    ];
    protected $casts = [
        'reviewed_at' => 'datetime',
        'privacy_consent_at' => 'datetime',
    ];
    /**
     * Sector associado ao interesse.
     */
    public function 
sector(
)
    {
        return $this->
belongsTo(Sector::
class);
    }
    /**
     * Oportunidade associada ao interesse.
     */
    public function 
opportunity(
)
    {
        return $this->
belongsTo(Opportunity::
class);
    }
    /**
     * Utilizador que analisou o pedido.
     */
    public function 
reviewer(
)
    {
        return $this->
belongsTo(User::
class, 'reviewed_by');
    }
    /**
     * Scope para pedidos novos.
     */
    public function scopeNew($query)
    {
        return $query->
where(
'status', 
self::
STATUS_NEW);
    }
    /**
     * Scope por estado.
     */
    public function 
scopeStatus(
$query, string $status)
    {
        return $query->
where(
'status', $status);
    }
    /**
     * Scope por tipo de investidor.
     */
    public function 
scopeInvestorType(
$query, string $type)
    {
        return $query->
where(
'investor_type', $type);
    }
    /**
     * Scope por sector.
     */
    public function 
scopeSector(
$query, int $sectorId)
    {
        return $query->
where(
'sector_id', $sectorId);
    }
    /**
     * Verifica se o pedido ainda está novo.
     */
    public function 
isNew(
): bool
    {
        return $this->status === 
self::
STATUS_NEW;
    }
    /**
     * Marcar como em análise.
     */
    public function 
markAsInReview(
?int $reviewerId = null): void
    {
        $this->
update(
[
            'status' => 
self::
STATUS_IN_REVIEW,
            'reviewed_by' => $reviewerId,
            'reviewed_at' => 
now(
),
        ]);
    }
    /**
     * Marcar como contactado.
     */
    public function 
markAsContacted(
?int $reviewerId = null): void
    {
        $this->
update(
[
            'status' => 
self::
STATUS_CONTACTED,
            'reviewed_by' => $reviewerId,
            'reviewed_at' => 
now(
),
        ]);
    }
    /**
     * Gerar código de referência.
     */
    public static function 
generateReferenceCode(
): string
    {
        $year = now
()-
>format('Y');
        do {
            $code = 'INT-
' .
 $
year .
 '-' . strtoupper(substr(md5(uniqid((string) mt_
rand(
), true)), 0, 8));
        } while (
self::where(
'reference_code', $
code)-
>
exists(
));
        return $code;
    }
    /**
     * Estados disponíveis.
     */
    public static function 
statuses(
): array
    {
        return [
            
self::
STATUS_NEW,
            
self::
STATUS_IN_REVIEW,
            
self::
STATUS_CONTACTED,
            
self::
STATUS_QUALIFIED,
            
self::
STATUS_REJECTED,
            
self::
STATUS_CLOSED,
        ];
    }
    /**
     * Tipos de investidor disponíveis.
     */
    public static function 
investorTypes(
): array
    {
        return [
            
self::
INVESTOR_TYPE_INDIVIDUAL,
            
self::
INVESTOR_TYPE_COMPANY,
            
self::
INVESTOR_TYPE_FUND,
            
self::
INVESTOR_TYPE_FINANCIAL_INSTITUTION,
            
self::
INVESTOR_TYPE_PARTNER,
            
self::
INVESTOR_TYPE_OTHER,
        ];
    }
}
3. Controller InterestRequestController.php
Caminho recomendado:
app/Http/Controllers/Api/V1/InterestRequestController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInterestRequestRequest;
use App\Models\InterestRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class InterestRequestController extends Controller
{
    /**
     * Listar manifestações de interesse.
     *
     * Esta rota deve ser protegida por autenticação/admin em produção.
     */
    public function 
index(
Request $request): JsonResponse
    {
        $query = 
InterestRequest::query(
)
            ->
with(
[
                'sector:
id,name
,slug',
                'opportunity:
id,title
,
slug,reference
',
                'reviewer:
id,name
',
            ])
            ->
latest(
);
        if ($request->filled('status')) {
            $query->
where(
'status', $request->string('status'));
        }
        if ($request->filled('investor_type')) {
            $query->
where(
'investor_type', $request->string('investor_type'));
        }
        if ($request->filled('sector_id')) {
            $query->
where(
'sector_id', $request->integer('sector_id'));
        }
        if ($request->filled('opportunity_id')) {
            $query->
where(
'opportunity_id', $request->integer('opportunity_id'));
        }
        if ($request->filled('q')) {
            $search = trim((string) $request->get('q'));
            $query->
where(
function ($subQuery) use ($search) {
                $subQuery->
where(
'name', 'like', "%{$
search}%
")
                    ->
orWhere(
'email', 'like', "%{$
search}%
")
                    ->
orWhere(
'company_name', 'like', "%{$
search}%
")
                    ->
orWhere(
'reference_code', 'like', "%{$
search}%
");
            });
        }
        $perPage = min((int) $request->
get(
'per_page', 15), 50);
        return 
response(
)->
json(
[
            'status' => 'success',
            'data' => $query->paginate($perPage),
        ]);
    }
    /**
     * Guardar nova manifestação de interesse.
     */
    public function 
store(
StoreInterestRequestRequest $request): JsonResponse
    {
        $validated = $request->
validated(
);
        // Campo honeypot 
anti-spam
.
        // Se vier preenchido, simula sucesso sem gravar.
        if 
(!empty
($validated['website'] ?? null)) {
            return 
response(
)->
json(
[
                'status' => 'success',
                'message' => 'Pedido recebido com sucesso.',
            ], 201);
        }
        try {
            $interestRequest = 
InterestRequest::create(
[
                'reference_code' => 
InterestRequest::generateReferenceCode(
),
                'name' => $validated['name'],
                'email' => $validated['email'],
                'phone' => $validated['phone'] ?? null,
                'country' => $validated['country'] ?? null,
                'company_name' => $validated['company_name'] ?? null,
                'investor_type' => $validated['investor_type'],
                'investment_range' => $validated['investment_range'] ?? null,
                'sector_id' => $validated['sector_id'] ?? null,
                'opportunity_id' => $validated['opportunity_id'] ?? null,
                'message' => $validated['message'],
                'source_page' => $validated['source_page'] ?? null,
                'status' => 
InterestRequest::
STATUS_NEW,
                'privacy_consent_at' => 
now(
),
                'ip_address' => $request->
ip(
),
                'user_agent' => $request->
userAgent(
),
            ]);
            return 
response(
)->
json(
[
                'status' => 'success',
                'message' => 'Manifestação de interesse enviada com sucesso.',
                'data' => [
                    'id' => $interestRequest->id,
                    'reference_code' => $interestRequest->reference_code,
                    'status' => $interestRequest->status,
                ],
            ], 201);
        } catch (\Throwable $exception) {
            
Log::error(
'Erro ao registar manifestação de interesse.', [
                'message' => $exception->
getMessage(
),
                'email' => $validated['email'] ?? null,
            ]);
            return 
response(
)->
json(
[
                'status' => 'error',
                'message' => 'Não foi possível registar a manifestação de interesse. Tente novamente mais tarde.',
            ], 500);
        }
    }
    /**
     * Mostrar uma manifestação de interesse.
     *
     * Esta rota deve ser protegida por autenticação/admin em produção.
     */
    public function 
show(
InterestRequest $interestRequest): JsonResponse
    {
        $interestRequest->
load(
[
            'sector:
id,name
,slug',
            'opportunity:
id,title
,
slug,reference
',
            'reviewer:
id,name
',
        ]);
        return 
response(
)->
json(
[
            'status' => 'success',
            'data' => $interestRequest,
        ]);
    }
    /**
     * Actualizar estado ou notas internas.
     *
     * Esta rota deve ser protegida por autenticação/admin em produção.
     */
    public function 
update(
Request $request, InterestRequest $interestRequest): JsonResponse
    {
        $validated = $request->
validate(
[
            'status' => [
                'sometimes',
                'string',
                'in:
new,in
_
review,contacted
,
qualified,rejected
,closed',
            ],
            'internal_notes' => [
                'nullable',
                'string',
                'max:5000',
            ],
        ]);
        if (isset($validated['status'])) {
            $interestRequest->status = $validated['status'];
            $interestRequest->reviewed_at = 
now(
);
            if ($request->
user(
)) {
                $interestRequest->reviewed_by = $request->
user(
)->id;
            }
        }
        if (array_key_
exists(
'internal_notes', $validated)) {
            $interestRequest->internal_notes = $validated['internal_notes'];
        }
        $interestRequest->
save(
);
        return 
response(
)->
json(
[
            'status' => 'success',
            'message' => 'Manifestação de interesse actualizada com sucesso.',
            'data' => $interestRequest,
        ]);
    }
    /**
     * Remover manifestação de interesse.
     *
     * Esta rota deve ser protegida por autenticação/admin em produção.
     */
    public function 
destroy(
InterestRequest $interestRequest): JsonResponse
    {
        $interestRequest->
delete(
);
        return 
response(
)->
json(
[
            'status' => 'success',
            'message' => 'Manifestação de interesse removida com sucesso.',
        ]);
    }
}
Rotas sugeridas em routes/api.php
Para ligar o controller:
<?php
use App\Http\Controllers\Api\V1\InterestRequestController;
use Illuminate\Support\Facades\Route;
Route::
prefix('v1
')-
>
group(
function () {
    
Route::post(
'/interest-requests', [
InterestRequestController::
class, 'store'])
        ->middleware('throttle:10,1');
    // Rotas administrativas — proteger com autenticação em produção.
    
Route::get(
'/interest-requests', [
InterestRequestController::
class, 'index']);
    
Route::
get('/interest-requests/{interestRequest}', [
InterestRequestController::
class, 'show']);
    
Route::
patch('/interest-requests/{interestRequest}', [
InterestRequestController::
class, 'update']);
    
Route::
delete('/interest-requests/{interestRequest}', [
InterestRequestController::
class, 'destroy']);
});
Observação importante
Este InterestRequestController.php depende do Form Request:
App\Http\Requests\StoreInterestRequestRequest
app/Http/Requests/StoreInterestRequestRequest.php
1. app/Http/Controllers/Api/V1/InterestRequestController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInterestRequestRequest;
use App\Models\InterestRequest;
use App\Models\Opportunity;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class InterestRequestController extends Controller
{
    public function 
store(
StoreInterestRequestRequest $request): JsonResponse
    {
        $validated = $request->
validated(
);
        try {
            $interestRequest = 
DB::transaction(
function () use ($request, $validated) {
                $opportunity = null;
                if 
(!empty
($validated['opportunityReference'])) {
                    $opportunity = 
Opportunity::query(
)
                        ->
where(
'reference', $validated['opportunityReference'])
                        ->
first(
);
                }
                return 
InterestRequest::create(
[
                    'opportunity_id' => $
opportunity?-
>id,
                    'reference' => $this->
generateReference(
),
                    'full_name' => trim($validated['fullName']),
                    'email' => strtolower(trim($validated['email'])),
                    'phone' => $validated['phone'] ?? null,
                    'investor_type' => $validated['investorType'],
                    'country' => $validated['country'],
                    'sector_interest' => $validated['sectorInterest'],
                    'opportunity_reference' => $validated['opportunityReference'] ?? null,
                    'investment_range' => $validated['investmentRange'] ?? null,
                    'message' => trim($validated['message']),
                    'privacy_consent' => (bool) $validated['privacyConsent'],
                    'consented_at' => 
now(
),
                    'status' => 'received',
                    'source_page' => $request->headers->get('referer'),
                    'ip_address' => $request->
ip(
),
                    'user_agent' => $request->
userAgent(
),
                    'metadata' => [
                        'locale' => $request->
input(
'locale', 'pt'),
                        'submitted_via' => 'public_interest_form',
                        'utm_source' => $request->input('utm_source'),
                        'utm_medium' => $request->input('utm_medium'),
                        'utm_campaign' => $request->input('utm_campaign'),
                    ],
                ]);
            });
            return 
response(
)->
json(
[
                'success' => true,
                'message' => 'Manifestação de interesse registada com sucesso.',
                'data' => [
                    'reference' => $interestRequest->reference,
                    'status' => $interestRequest->status,
                    'nextStep' => 'A equipa do portal analisará o pedido e poderá solicitar informação adicional.',
                    'submittedAt' => $interestRequest->created_
at?-
>toIso8601
String(
),
                ],
            ], 201);
        } catch (\Throwable $exception) {
            
Log::error(
'Erro ao registar manifestação de interesse', [
                'message' => $exception->
getMessage(
),
                'trace_id' => 
Str::
uuid
()-
>
toString(
),
            ]);
            return 
response(
)->
json(
[
                'success' => false,
                'message' => 'Não foi possível registar a manifestação de interesse. Tente novamente mais tarde.',
            ], 500);
        }
    }
    private function 
generateReference(
): string
    {
        $year = now
()-
>format('Y');
        $count = 
InterestRequest::withTrashed(
)
            ->
whereYear(
'created_at', $year)
            ->
count(
) + 1;
        return 
sprintf(
'IR-%s-%06d', $year, $count);
    }
}
2. Migration — database/migrations/xxxx_xx_xx_xxxxxx_create_interest_requests_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    public function 
up(
): void
    {
        
Schema::create(
'interest_requests', function (Blueprint $table) {
            $table->
id(
);
            $table->foreignId('opportunity_id')
                ->
nullable(
)
                ->constrained('opportunities')
                ->
cascadeOnUpdate(
)
                ->
nullOnDelete(
);
            $table->
string(
'reference', 
80)-
>
unique(
);
            $table->
string(
'full_name', 150);
            $table->
string(
'email', 150);
            $table->
string(
'phone', 
30)-
>
nullable(
);
            $table->
string(
'investor_type', 80);
            $table->
string(
'country', 100);
            $table->
string(
'sector_interest', 150);
            $table->
string(
'opportunity_reference', 
80)-
>
nullable(
);
            $table->
string(
'investment_range', 
80)-
>
nullable(
);
            $table->text('message');
            $table->boolean('privacy_consent
')-
>default(false);
            $table->timestamp('consented_at
')-
>
nullable(
);
            $table->
string(
'status', 
50)-
>default('received');
            $table->
string(
'source_page', 
255)-
>
nullable(
);
            $table->ipAddress('ip_address
')-
>
nullable(
);
            $table->text('user_agent
')-
>
nullable(
);
            $table->json('metadata
')-
>
nullable(
);
            $table->timestamp('reviewed_at
')-
>
nullable(
);
            $table->foreignId('reviewed_by')
                ->
nullable(
)
                ->constrained('users')
                ->
nullOnDelete(
);
            $table->
timestamps(
);
            $table->
softDeletes(
);
            $table->index('reference');
            $table->index('email');
            $table->index('investor_type');
            $table->index('sector_interest');
            $table->index('opportunity_reference');
            $table->index('status');
            $table->index('created_at');
            $table->index('opportunity_id');
        });
    }
    public function 
down(
): void
    {
        
Schema::
dropIfExists('interest_requests');
    }
};
3. Model necessário — app/Models/InterestRequest.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InterestRequest extends Model
{
    use SoftDeletes;
    protected $fillable = [
        'opportunity_id',
        'reference',
        'full_name',
        'email',
        'phone',
        'investor_type',
        'country',
        'sector_interest',
        'opportunity_reference',
        'investment_range',
        'message',
        'privacy_consent',
        'consented_at',
        'status',
        'source_page',
        'ip_address',
        'user_agent',
        'metadata',
        'reviewed_at',
        'reviewed_by',
    ];
    protected $casts = [
        'privacy_consent' => 'boolean',
        'consented_at' => 'datetime',
        'metadata' => 'array',
        'reviewed_at' => 'datetime',
    ];
    public function 
opportunity(
): BelongsTo
    {
        return $this->
belongsTo(Opportunity::
class);
    }
    public function 
reviewer(
): BelongsTo
    {
        return $this->
belongsTo(User::
class, 'reviewed_by');
    }
}
4. StoreInterestRequestRequest.php
Este ficheiro deve ficar em:
app/Http/Requests/StoreInterestRequestRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreInterestRequestRequest extends FormRequest
{
    public function 
authorize(
): bool
    {
        return true;
    }
    public function 
rules(
): array
    {
        return [
            'fullName' => ['required', 'string', 'min:3', 'max:150'],
            'email' => ['required', 'email:
rfc,dns
', 'max:150'],
            'phone' => ['nullable', 'string', 'max:30'],
            'investorType' => [
                'required',
                'string',
                
Rule::in(
[
                    'individual',
                    'empresa',
                    'fundo',
                    'entidade_publica',
                    'parceiro_tecnico',
                    'outro',
                ]),
            ],
            'country' => ['required', 'string', 'max:100'],
            'sectorInterest' => ['required', 'string', 'max:150'],
            'opportunityReference' => ['nullable', 'string', 'max:80'],
            'investmentRange' => ['nullable', 'string', 'max:80'],
            'message' => ['required', 'string', 'min:20', 'max:2000'],
            'privacyConsent' => ['required', 'accepted'],
            'locale' => ['nullable', 'string', 
Rule::in(
['pt', 'en'])],
            'utm_source' => ['nullable', 'string', 'max:120'],
            'utm_medium' => ['nullable', 'string', 'max:120'],
            'utm_campaign' => ['nullable', 'string', 'max:120'],
        ];
    }
    public function 
messages(
): array
    {
        return [
            'fullName.required' => 'O nome completo é obrigatório.',
            'fullName.min' => 'O nome completo deve ter pelo menos 3 caracteres.',
            'fullName.max' => 'O nome completo não deve exceder 150 caracteres.',
            '
email.required
' => 'O email é obrigatório.',
            '
email.email
' => 'Indique um email válido.',
            'email.max' => 'O email não deve exceder 150 caracteres.',
            'phone.max' => 'O telefone não deve exceder 30 caracteres.',
            'investorType.required' => 'Seleccione o tipo de investidor.',
            'investorType.in' => 'O tipo de investidor seleccionado não é válido.',
            '
country.required
' => 'O país é obrigatório.',
            'country.max' => 'O país não deve exceder 100 caracteres.',
            'sectorInterest.required' => 'O sector de interesse é obrigatório.',
            'sectorInterest.max' => 'O sector de interesse não deve exceder 150 caracteres.',
            'opportunityReference.max' => 'A referência da oportunidade não deve exceder 80 caracteres.',
            'investmentRange.max' => 'A faixa de investimento não deve exceder 80 caracteres.',
            '
message.required
' => 'A mensagem é obrigatória.',
            'message.min' => 'A mensagem deve ter pelo menos 20 caracteres.',
            'message.max' => 'A mensagem não deve exceder 2000 caracteres.',
            'privacyConsent.required' => 'É necessário aceitar a política de privacidade.',
            'privacyConsent.accepted' => 'É necessário aceitar a política de privacidade.',
        ];
    }
    public function 
attributes(
): array
    {
        return [
            'fullName' => 'nome completo',
            'email' => 'email',
            'phone' => 'telefone',
            'investorType' => 'tipo de investidor',
            'country' => 'país',
            'sectorInterest' => 'sector de interesse',
            'opportunityReference' => 'referência da oportunidade',
            'investmentRange' => 'faixa de investimento',
            'message' => 'mensagem',
            'privacyConsent' => 'consentimento de privacidade',
        ];
    }
}
5. Como integrar o StoreInterestRequestRequest.php no controller
A integração é feita directamente no método 
store(
) do controller.
Em vez de usar:
public function 
store(
Request $request)
usa-se:
public function 
store(
StoreInterestRequestRequest $request)
Ou seja, o Laravel executa automaticamente:
autorização através do método:
authorize(
)
validação através do método:
rules(
)
mensagens personalizadas através de:
messages(
)
atributos amigáveis através de:
attributes(
)
Depois, dentro do controller, basta chamar:
$validated = $request->
validated(
);
6. Rota em routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use::post(
'/interest-requests', [
InterestRequestController::
class, 'store'
])use
 App\Http\Controllers\Api\V1\InterestRequestController;
            ->middleware('throttle:10,1');
    });
Route::
prefix('v1')
    ->middleware(['throttle:api'])
    ->
group(
function () {
7. Payload JSON — pedido POST /api/v1/interest-requests
{
  "fullName": "Nome do Investidor",
  "email": "investidor@empresa.com",
  "phone": "+258 84 000 0000",
  "investorType": "empresa",
  "country": "Moçambique",
  "sectorInterest": "Agronegócio",
  "opportunityReference": "INV-MZ-AGR-001",
  "investmentRange": "1000000-5000000",
  "message": "Pretendo receber mais informação sobre esta oportunidade e compreender os requisitos aplicáveis ao investimento.",
  "privacyConsent": true,
  "locale": "pt"
}
8. Resposta JSON — sucesso
{
  "success": true,
  "message": "Manifestação de interesse registada com sucesso.",
  "data": {
    "reference": "IR-2026-000001",
    "status": "received",
    "nextStep": "A equipa do portal analisará o pedido e poderá solicitar informação adicional.",
    "submittedAt": "2026-08-27T10:34:00+02:00"
  }
}
9. Resposta JSON — erro de validação
{
  "message": "The given data was invalid.",
  "errors": {
    "email": [
      "Indique um email válido."
    ],
    "privacyConsent": [
      "É necessário aceitar a política de privacidade."
    ]
  }
}
10. Comandos úteis
php artisan make:request StoreInterestRequestRequest
php artisan make:model InterestRequest -m
php artisan make:controller Api/V1/InterestRequestController
php artisan migrate
Observação importante
A ordem recomendada de criação é:
criar a migration interest_requests;
criar o model InterestRequest;
criar o StoreInterestRequestRequest;
criar o InterestRequestController;
adicionar a rota em routes/api.php;
testar o endpoint com Postman, Insomnia ou frontend React.
Segue o código completo e copiável para:
HeroSection.jsx
SectorCard.jsx
SectorGrid.jsx
HomePage.jsx com integração
Testes básicos Laravel para o endpoint /api/v1/home
1. src/components/home/HeroSection.jsx
export default function 
HeroSection(
{
  hero,
  searchSuggestions = [],
  searchTerm,
  setSearchTerm,
  onSearchSubmit,
}) {
  const title =
    
hero?.
title ||
    "Invista em Moçambique com informação clara e oportunidades estruturadas";
  const subtitle =
    
hero?.
subtitle ||
    "Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas.";
  const primaryButton = 
hero?.
primaryButton || {
    label: "Explorar oportunidades",
    url: "/oportunidades",
  };
  const secondaryButton = 
hero?.
secondaryButton || {
    label: "Como investir",
    url: "/guia-do-investidor",
  };
  return (
    <section className="bg-blue-950 text-white">
      <div className="max-w-7xl mx-auto px-6 py-20 grid lg:grid-cols-2 gap-12 items-center">
        <div>
          <span className="inline-flex px-4 py-2 rounded-full bg-white/10 text-blue-100 text-sm font-semibold">
            INVESTE Moçambique
          </span>
          <h1 className="mt-6 text-4xl md:text-5xl font-bold leading-tight">
            {title}
          </h1>
          <p className="mt-6 text-lg text-blue-100 leading-relaxed max-w-2xl">
            {subtitle}
          </p>
          <div className="mt-8 flex flex-wrap gap-4">
            <a
              href={primaryButton.url}
              className="inline-flex items-center justify-center px-6 py-3 bg-green-500 text-blue-950 rounded-lg font-bold hover:bg-green-400"
            >
              {primaryButton.label}
            </a>
            <a
              href={secondaryButton.url}
              className="inline-flex items-center justify-center px-6 py-3 border border-white/40 rounded-lg font-semibold hover:bg-white/10"
            >
              {secondaryButton.label}
            </a>
          </div>
        </div>
        <div className="bg-white/10 border border-white/10 rounded-2xl p-6 backdrop-blur">
          <h2 className="text-xl font-bold">Pesquisa rápida de informação</h2>
          <p className="mt-2 text-blue-100">
            Pesquise oportunidades, incentivos, requisitos legais ou temas
            relacionados ao investimento.
          </p>
          <form
            onSubmit={onSearchSubmit}
            className="mt-6 flex flex-col sm:flex-row gap-3"
          >
            <input
              type="text"
              value={searchTerm}
              onChange={(event) => setSearchTerm(
event.target
.value)}
              placeholder="Ex.: benefícios fiscais, APIEX, ZEE..."
              className="w-full px-4 py-3 rounded-lg text-gray-900 outline-none focus:ring-2 focus:ring-green-400"
            />
            <button
              type="submit"
              className="px-5 py-3 bg-green-500 text-blue-950 rounded-lg font-bold hover:bg-green-400"
            >
              Pesquisar
            </button>
          </form>
          {searchSuggestions.length > 0 && (
            <div className="mt-5 flex flex-wrap gap-2">
              {searchSuggestions.map((suggestion) => (
                <button
                  key={suggestion}
                  type="button"
                  onClick
={
() => setSearchTerm(suggestion)}
                  className="text-sm bg-white/15 hover:bg-white/25 px-3 py-1 rounded-full"
                >
                  {suggestion}
                </button>
              ))}
            </div>
          )}
        </div>
      </div>
    </section>
  );
}
2. src/components/sectors/SectorCard.jsx
export default function 
SectorCard({ sector
 }) {
  if 
(!sector
) return null;
  const title = sector.name || "Sector";
  const description =
    
sector.shortDescription
 ||
    
sector.short
_description ||
    
sector.description
 ||
    "Informação sectorial em validação.";
  const opportunitiesCount =
    
sector.opportunitiesCount
 ?? 
sector.opportunities
_
count ??
 0;
  const url = sector.url || `/sectores/${
sector.slug
}`;
  return (
    <article className="group bg-white border border-gray-200 rounded-2xl p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all duration-200">
      <div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-900 flex items-center justify-center font-bold mb-5">
        {
sector.icon
 ? (
          <span className="text-sm uppercase">{
sector.icon
}</span>
        ) : (
          <span>INV</span>
        )}
      </div>
      <h3 className="text-xl font-bold text-blue-950 group-hover:text-blue-800 transition">
        {title}
      </h3>
      <p className="mt-3 text-gray-600 leading-relaxed">{description}</p>
      <div className="mt-6 flex items-center justify-between gap-4">
        <span className="text-sm text-gray-500">
          {opportunitiesCount
}{
" "}
          {opportunitiesCount === 
1 ?
 "oportunidade
" :
 "oportunidades"}
        </span>
        <a
          href={url}
          className="text-sm font-semibold text-blue-800 hover:text-blue-600"
        >
          Ver sector
        </a>
      </div>
    </article>
  );
}
3. src/components/sectors/SectorGrid.jsx
import 
{ useEffect
, 
useState }
 from "react";
import SectorCard from 
"./
SectorCard";
import 
{ getSectors
 } from 
"../..
/services/opportunityService";
export default function 
SectorGrid(
{
  title = "Sectores estratégicos",
  subtitle = "Explore áreas prioritárias de investimento em Moçambique.",
  initialSectors = null,
  showHeader = true,
}) {
  const [sectors, setSectors] = 
useState(
initialSectors || []);
  const [loading, setLoading] = useState
(!initialSectors
);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    if (initialSectors) return;
    let active = true;
    async function 
loadSectors(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await 
getSectors(
);
        if 
(!active
) return;
        const data = response?.data || response || [];
        
setSectors(
Array.isArray(data
) ?
 
data :
 []);
      } catch (err) {
        if 
(!active
) return;
        
setError(
          
err?.
message || "Não foi possível carregar os sectores estratégicos."
        );
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadSectors(
);
    return () => {
      active = false;
    };
  }, [initialSectors]);
  if (loading) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="animate-pulse">
          <div className="h-8 bg-gray-200 rounded w-64 mb-4" />
          <div className="h-4 bg-gray-200 rounded w-full max-w-2xl mb-8" />
          <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
            {[1, 2, 3
].map
((item) => (
              <div
                key={item}
                className="h-56 bg-gray-100 rounded-2xl border border-gray-200"
              />
            ))}
          </div>
        </div>
      </section>
    );
  }
  if (error) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        <div className="bg-red-50 border border-red-200 text-red-700 p-5 rounded-xl">
          <p className="font-semibold">Erro ao carregar sectores</p>
          <p className="mt-1 text-sm">{error}</p>
        </div>
      </section>
    );
  }
  if 
(!sectors
.length) {
    return (
      <section className="max-w-7xl mx-auto px-6 py-12">
        {showHeader && (
          <div className="mb-6">
            <h2 className="text-3xl font-bold text-blue-950">{title}</h2>
            {subtitle && (
              <p className="mt-3 text-gray-600 max-w-3xl">{subtitle}</p>
            )}
          </div>
        )}
        <div className="bg-gray-50 border border-gray-200 rounded-xl p-6">
          <p className="text-gray-600">
            Ainda não existem sectores publicados.
          </p>
        </div>
      </section>
    );
  }
  return (
    <section className="max-w-7xl mx-auto px-6 py-12">
      {showHeader && (
        <div className="mb-8">
          <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
            Prioridades de investimento
          </span>
          <h2 className="mt-2 text-3xl font-bold text-blue-950">{title}</h2>
          {subtitle && (
            <p className="mt-3 text-gray-600 max-w-3xl">{subtitle}</p>
          )}
        </div>
      )}
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        {sectors.map((sector) => (
          <SectorCard
            key
={sector.slug
 || sector.id || sector.name}
            sector={sector}
          />
        ))}
      </div>
    </section>
  );
}
4. src/pages/HomePage.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
import HeroSection from 
"..
/components/home/HeroSection";
import SectorGrid from 
"..
/components/sectors/SectorGrid";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
export default function 
HomePage(
) {
  const [home, setHome] = useState(null);
  const [locale, setLocale] = useState("pt");
  const [searchTerm, setSearchTerm] = 
useState(
"");
  const [loading, setLoading] = useState(true);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    let active = true;
    async function 
loadHome(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await getHomeData(locale);
        if 
(!active
) return;
        
setHome(
response?.data || response);
      } catch (err) {
        if 
(!active
) return;
        
setError(
          
err?.
message ||
            "Não foi possível carregar os dados da página principal."
        );
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadHome(
);
    return () => {
      active = false;
    };
  }, [locale]);
  function handleSearchSubmit(event) {
    
event.preventDefault
();
    const query = searchTerm.trim();
    if 
(!query
) return;
    
window.location
.href = `/oportunidades?query=${encodeURIComponent(query
)}`
;
  }
  if (loading) {
    return (
      <main className="min-h-screen flex items-center justify-center bg-gray-50">
        <div className="text-center">
          <div className="w-12 h-12 mx-auto border-4 border-blue-200 border-t-blue-900 rounded-full animate-spin" />
          <p className="mt-4 text-gray-600">
            A carregar o Portal do Investidor...
          </p>
        </div>
      </main>
    );
  }
  if (error) {
    return (
      <main className="min-h-screen flex items-center justify-center bg-gray-50 px-6">
        <div className="max-w-lg bg-white border border-red-200 rounded-2xl p-8 shadow-sm text-center">
          <h1 className="text-2xl font-bold text-red-700">
            Erro ao carregar a página
          </h1>
          <p className="mt-3 text-gray-600">{error}</p>
          <button
            onClick
={
() => 
window.location
.
reload(
)}
            className="mt-6 px-5 py-3 rounded-lg bg-blue-950 text-white font-semibold hover:bg-blue-800"
          >
            Tentar novamente
          </button>
        </div>
      </main>
    );
  }
  if 
(!home
) return null;
  const hero = 
home.hero
 || {};
  const quickActions = 
home.quickActions
 || [];
  const sectors = 
home.sectors
 || [];
  const featuredOpportunities = 
home.featuredOpportunities
 || [];
  const investorJourney = 
home.investorJourney
 || [];
  const incentivesPreview = 
home.incentivesPreview
 || [];
  const legalHighlights = 
home.legalHighlights
 || [];
  const partnershipExchange = 
home.partnershipExchange
 || null;
  const stats = 
home.stats
 || {};
  const searchSuggestions = 
home.searchSuggestions
 || [];
  return (
    <main className="min-h-screen bg-gray-50 text-gray-900">
      <Header locale={locale} setLocale={setLocale} />
      <HeroSection
        hero={hero}
        searchSuggestions={searchSuggestions}
        searchTerm={searchTerm}
        setSearchTerm={setSearchTerm}
        onSearchSubmit={handleSearchSubmit}
      />
      {quickActions.length > 0 && (
        <section className="max-w-7xl mx-auto px-6 py-10">
          <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
            {quickActions.map((action) => (
              <a
                key={
action.label
}
                href={action.url}
                className="bg-white border border-gray-200 rounded-xl p-5 shadow-sm hover:shadow-md transition"
              >
                <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                  Acesso rápido
                </span>
                <h3 className="mt-2 font-bold text-blue-950">
                  {
action.label
}
                </h3>
                <p className="mt-2 text-sm text-gray-500">Aceder agora</p>
              </a>
            ))}
          </div>
        </section>
      )}
      <SectorGrid initialSectors={sectors} />
      <FeaturedOpportunities initialOpportunities={featuredOpportunities} />
      {investorJourney.length > 0 && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="mb-8">
            <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
              Guia do investidor
            </span>
            <h2 className="mt-2 text-3xl font-bold text-blue-950">
              Como investir em Moçambique
            </h2>
            <p className="mt-3 text-gray-600 max-w-3xl">
              Siga um percurso simples para identificar oportunidades,
              compreender requisitos e iniciar contacto.
            </p>
          </div>
          <div className="grid md:grid-cols-3 gap-6">
            {investorJourney.map((step) => (
              <div
                key={
step.step
}
                className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm"
              >
                <div className="w-11 h-11 rounded-full bg-blue-950 text-white flex items-center justify-center font-bold">
                  {
step.step
}
                </div>
                <h3 className="mt-5 text-xl font-bold text-blue-950">
                  {
step.title
}
                </h3>
                <p className="mt-3 text-gray-600 leading-relaxed">
                  {
step.description
}
                </p>
              </div>
            ))}
          </div>
        </section>
      )}
      {(incentivesPreview.length > 0 || legalHighlights.length > 0) && (
        <section className="bg-blue-50 py-12">
          <div className="max-w-7xl mx-auto px-6 grid lg:grid-cols-2 gap-8">
            <div>
              <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                Incentivos
              </span>
              <h2 className="mt-2 text-3xl font-bold text-blue-950">
                Incentivos e benefícios
              </h2>
              <div className="mt-6 space-y-4">
                {incentivesPreview.map((item) => (
                  <a
                    key={
item.title
}
                    href={item.url}
                    className="block bg-white border border-blue-100 rounded-xl p-5 hover:shadow-md transition"
                  >
                    <h3 className="font-bold text-blue-950">{
item.title
}</h3>
                    <p className="mt-2 text-gray-600">{
item.description
}</p>
                  </a>
                ))}
              </div>
            </div>
            <div>
              <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                Recursos legais
              </span>
              <h2 className="mt-2 text-3xl font-bold text-blue-950">
                Documentos em destaque
              </h2>
              <div className="mt-6 space-y-4">
                {legalHighlights.map((item) => (
                  <a
                    key={
item.title
}
                    href={item.url}
                    className="block bg-white border border-blue-100 rounded-xl p-5 hover:shadow-md transition"
                  >
                    <span className="text-sm text-gray-500">
                      {
item.category
}
                    </span>
                    <h3 className="mt-1 font-bold text-blue-950">
                      {
item.title
}
                    </h3>
                  </a>
                ))}
              </div>
            </div>
          </div>
        </section>
      )}
      {partnershipExchange && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="bg-blue-950 text-white rounded-2xl p-8 md:p-10">
            <span className="text-sm uppercase tracking-wide text-green-300 font-semibold">
              Parcerias e capital
            </span>
            <h2 className="mt-3 text-3xl font-bold">
              {partnershipExchange.title}
            </h2>
            <p className="mt-3 text-blue-100 max-w-3xl">
              {partnershipExchange.description}
            </p>
            <div className="mt-6 flex flex-wrap gap-4">
              {
partnershipExchange.actions?.
map((action) => (
                <a
                  key={
action.label
}
                  href={action.url}
                  className="inline-flex items-center justify-center px-5 py-3 bg-white text-blue-950 rounded-lg font-semibold hover:bg-blue-50"
                >
                  {
action.label
}
                </a>
              ))}
            </div>
          </div>
        </section>
      )}
      <PortalStats stats={stats} />
      <Footer />
    </main>
  );
}
function 
Header({ locale
, 
setLocale }
) {
  return (
    <header className="bg-white border-b border-gray-200 sticky top-0 z-40">
      <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-6">
        <a href="/" className="text-xl font-black text-blue-950">
          INVESTE Moçambique
        </a>
        <nav className="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-700">
          <a href="/oportunidades" className="hover:text-blue-900">
            Oportunidades
          </a>
          <a href="/sectores" className="hover:text-blue-900">
            Sectores
          </a>
          <a href="/guia-do-investidor" className="hover:text-blue-900">
            Como investir
          </a>
          <a href="/incentivos" className="hover:text-blue-900">
            Incentivos
          </a>
          <a href="/recursos" className="hover:text-blue-900">
            Recursos legais
          </a>
        </nav>
        <div className="flex items-center gap-3">
          <button
            type="button"
            onClick
={
() => setLocale("pt")}
            className
={
              locale === "pt"
                ? "font-bold text-blue-950"
                : "text-gray-500 hover:text-blue-950"
            }
          >
            PT
          </button>
          <span className="text-gray-300">|</span>
          <button
            type="button"
            onClick
={
() => setLocale("en")}
            className
={
              locale === "en"
                ? "font-bold text-blue-950"
                : "text-gray-500 hover:text-blue-950"
            }
          >
            EN
          </button>
          <a
            href="/login"
            className="hidden sm:inline-flex px-4 py-2 rounded-lg bg-blue-950 text-white text-sm font-semibold hover:bg-blue-800"
          >
            Área do investidor
          </a>
        </div>
      </div>
    </header>
  );
}
function 
PortalStats({ stats
 }) {
  const items = [
    {
      label: "Oportunidades",
      value: 
stats?.opportunities ??
 0,
    },
    {
      label: "Sectores",
      value: 
stats?.sectors ??
 0,
    },
    {
      label: "Províncias",
      value: 
stats?.provinces ??
 0,
    },
    {
      label: "Pedidos recebidos",
      value: 
stats?.interestRequests ??
 0,
    },
  ];
  return (
    <section className="max-w-7xl mx-auto px-6 py-12">
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
        {items.map((item) => (
          <div
            key={
item.label
}
            className="bg-white border border-gray-200 rounded-2xl p-6 text-center shadow-sm"
          >
            <p className="text-3xl font-black text-blue-950">
              {Number(item.value
).toLocaleString
()}
            </p>
            <p className="mt-2 text-gray-600">{
item.label
}</p>
          </div>
        ))}
      </div>
      {
stats?.
updatedAt && (
        <p className="mt-4 text-sm text-gray-500 text-center">
          Dados actualizados em {
stats.updatedAt
}
        </p>
      )}
    </section>
  );
}
function 
Footer(
) {
  return (
    <footer className="bg-gray-950 text-white py-10">
      <div className="max-w-7xl mx-auto px-6 flex flex-col lg:flex-row justify-between gap-8">
        <div>
          <h2 className="font-black text-xl">INVESTE Moçambique</h2>
          <p className="mt-3 text-gray-300 max-w-md">
            Plataforma de informação, orientação e facilitação de oportunidades
            de investimento em Moçambique.
          </p>
        </div>
        <div className="grid sm:grid-cols-3 gap-8 text-sm">
          <div>
            <h3 className="font-bold mb-3">Portal</h3>
            <ul className="space-y-2 text-gray-300">
              <li>
                <a href="/oportunidades">Oportunidades</a>
              </li>
              <li>
                <a href="/sectores">Sectores</a>
              </li>
              <li>
                <a href="/incentivos">Incentivos</a>
              </li>
            </ul>
          </div>
          <div>
            <h3 className="font-bold mb-3">Apoio</h3>
            <ul className="space-y-2 text-gray-300">
              <li>
                <a href="/guia-do-investidor">Guia do investidor</a>
              </li>
              <li>
                <a href="/recursos">Recursos legais</a>
              </li>
              <li>
                <a href="/contactos">Contactos</a>
              </li>
            </ul>
          </div>
          <div>
            <h3 className="font-bold mb-3">Legal</h3>
            <ul className="space-y-2 text-gray-300">
              <li>
                <a href="/termos">Termos de uso</a>
              </li>
              <li>
                <a href="/privacidade">Política de privacidade</a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </footer>
  );
}
5. Teste Laravel — tests/Feature/HomeApiTest.php
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class HomeApiTest extends TestCase
{
    use RefreshDatabase;
    public function test_home_endpoint_returns_successful_
response(
): void
    {
        $response = $this->getJson('/api/v1/home');
        $response->
assertOk(
)
            ->
assertJson(
[
                'success' => true,
            ]);
    }
    public function test_home_endpoint_returns_expected_main_
keys(
): void
    {
        $response = $this->getJson('/api/v1/home?locale=pt');
        $response->
assertOk(
)
            ->
assertJsonStructure(
[
                'success',
                'locale',
                'data' => [
                    'hero',
                    'quickActions',
                    'searchSuggestions',
                    'sectors',
                    'featuredOpportunities',
                    'investorJourney',
                    'incentivesPreview',
                    'legalHighlights',
                    'partnershipExchange',
                    'stats',
                ],
            ]);
    }
    public function test_home_endpoint_accepts_valid_
locale(
): void
    {
        $response = $this->getJson('/api/v1/home?locale=en');
        $response->
assertOk(
)
            ->
assertJsonPath(
'success', true)
            ->
assertJsonPath(
'locale', 'en');
    }
    public function test_home_endpoint_rejects_invalid_
locale(
): void
    {
        $response = $this->getJson('/api/v1/home?locale=fr');
        $response->
assertStatus(
422)
            ->assertJsonValidationErrors(['locale']);
    }
    public function test_home_endpoint_contains_hero_
content(
): void
    {
        $response = $this->getJson('/api/v1/home?locale=pt');
        $response->
assertOk(
)
            ->
assertJsonStructure(
[
                'data' => [
                    'hero' => [
                        'title',
                        'subtitle',
                        'primaryButton' => [
                            'label',
                            'url',
                        ],
                        'secondaryButton' => [
                            'label',
                            'url',
                        ],
                    ],
                ],
            ]);
    }
}
6. Rota necessária — routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\HomeController;
Route::
prefix('v1')
    ->middleware(['throttle:api'])
    ->
group(
function () {
        
Route::get(
'/home', [
HomeController::
class, 'show']);
    });
7. Comandos úteis
php artisan make:test HomeApiTest
php artisan test --filter=HomeApiTest
1. homeService.js
Caminho recomendado:
src/services/homeService.js
// src/services/homeService.js
const API_URL = 
import.meta.env.VITE
_API_URL || "http://localhost:8000";
class ApiError extends Error {
  
constructor(
message, status, payload = null) {
    super(message);
    this.name = "ApiError";
    
this.status
 = status;
    
this.payload
 = payload;
    
this.errors
 = 
payload?.
errors || {};
  }
}
async function 
request(
path, options = {}) {
  const url = `${API_
URL}$
{path}`;
  const response = await 
fetch(
url, {
    method: 
options.method
 || "GET",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      ...
options.headers
,
    },
    ...options,
  });
  let payload = null;
  try {
    payload = await 
response.json
();
  } catch {
    payload = null;
  }
  if 
(!response
.ok) {
    throw new 
ApiError(
      
payload?.
message || "Não foi possível concluir o pedido à API.",
      
response.status
,
      payload
    );
  }
  return payload;
}
function 
buildQuery(
params = {}) {
  const query = new 
URLSearchParams(
);
  Object.entries(params
).forEach
(([key, value]) => {
    if (
value !
== undefined && 
value !
== null && 
value !
== "") {
      
query.append
(key, String(value));
    }
  });
  const queryString = 
query.toString
();
  return 
queryString ?
 
`?$
{queryString}
` :
 "";
}
export async function 
getHomeData(
locale = "pt") {
  const query = 
buildQuery({ locale
 });
  const payload = await request(`/api/v1/home${query}`);
  return payload?.data || payload;
}
export async function 
getFeaturedOpportunities(
{
  locale = "pt",
  limit = 6,
} = {}) {
  const query = 
buildQuery(
{
    locale,
    limit,
  });
  const payload = await request(`/api/v1/opportunities/featured${query}`);
  return payload?.data || [];
}
export async function 
getLatestOpportunities(
{
  locale = "pt",
  limit = 6,
} = {}) {
  const query = 
buildQuery(
{
    locale,
    limit,
  });
  const payload = await request(`/api/v1/opportunities/latest${query}`);
  return payload?.data || [];
}
export async function 
getSectors(
locale = "pt") {
  const query = 
buildQuery({ locale
 });
  const payload = await request(`/api/v1/sectors${query}`);
  return payload?.data || [];
}
export async function 
getPortalStats(
locale = "pt") {
  const query = 
buildQuery({ locale
 });
  const payload = await request(`/api/v1/portal-stats${query}`);
  return payload?.data || {};
}
export async function 
searchOpportunities(
{
  q = "",
  sector = "",
  province = "",
  maturity = "",
  financing_type = "",
  page = 1,
  per_page = 12,
  locale = "pt",
} = {}) {
  const query = 
buildQuery(
{
    q,
    sector,
    province,
    maturity,
    financing_type,
    page,
    per_page,
    locale,
  });
  const payload = await request(`/api/v1/opportunities/search${query}`);
  return payload;
}
export default {
  getHomeData,
  getFeaturedOpportunities,
  getLatestOpportunities,
  getSectors,
  getPortalStats,
  searchOpportunities,
};
2. FeaturedOpportunities.jsx
Caminho recomendado:
src/components/opportunities/FeaturedOpportunities.jsx
// src/components/opportunities/FeaturedOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
FeaturedOpportunities(
{
  title = "Oportunidades em destaque",
  description = "Projectos prioritários com maior relevância estratégica para investidores nacionais e estrangeiros.",
  opportunities = [],
  viewAllUrl = "/oportunidades",
  emptyMessage = "Ainda não existem oportunidades em destaque publicadas.",
}) {
  const hasOpportunities = Array.isArray(opportunities) && 
opportunities.length
 > 0;
  return (
    <section className="bg-slate-50 py-16">
      <div className="mx-auto max-w-7xl px-4">
        <div className="mb-8 flex flex-col justify-between gap-4 md:flex-row md:items-end">
          <div>
            <span className="mb-2 inline-flex rounded-full bg-blue-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700">
              Carteira de projectos
            </span>
            <h2 className="text-2xl font-bold text-slate-900 md:text-3xl">
              {title}
            </h2>
            {description && (
              <p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600 md:text-base">
                {description}
              </p>
            )}
          </div>
          {viewAllUrl && (
            <a
              href={viewAllUrl}
              className="inline-flex items-center justify-center rounded-xl border border-blue-700 px-5 py-3 text-sm font-semibold text-blue-700 transition hover:bg-blue-700 hover:text-white"
            >
              Ver todas →
            </a>
          )}
        </div>
        
{!hasOpportunities
 ? (
          <div className="rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center">
            <h3 className="text-lg font-semibold text-slate-900">
              Sem oportunidades em destaque
            </h3>
            <p className="mt-2 text-sm text-slate-600">{emptyMessage}</p>
          </div>
        ) : (
          <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
            {opportunities.map((opportunity) => (
              <OpportunityCard
                key
={
opportunity.id || 
opportunity.reference
 || 
opportunity.slug
}
                opportunity={opportunity}
              />
            ))}
          </div>
        )}
      </div>
    </section>
  );
}
HomeController.php para testes
Caminho recomendado:
app/Http/Controllers/Api/V1/HomeController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class HomeController extends Controller
{
    /**
     * Payload público inicial da página principal.
     *
     * Este controller usa dados mockados para testes iniciais de frontend.
     * Depois, pode ser substituído por consultas reais a models como:
     * Sector, Opportunity, LegalResource e InterestRequest.
     */
    public function 
show(
Request $request): JsonResponse
    {
        $locale = $request->
query(
'locale', 'pt');
        if (! in_
array(
$locale, ['pt', 'en'], true)) {
            $locale = 'pt';
        }
        return 
response(
)->
json(
[
            'status' => 'success',
            'locale' => $locale,
            'data' => [
                'hero' => [
                    'title' => 'Invista em Moçambique com informação clara e oportunidades estruturadas',
                    'subtitle' => 'Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento organizadas para investidores nacionais e estrangeiros.',
                    'primary_button' => [
                        'label' => 'Explorar oportunidades',
                        'url' => '/oportunidades',
                    ],
                    'secondary_button' => [
                        'label' => 'Como investir',
                        'url' => '/guia-do-investidor',
                    ],
                ],
                'quick_actions' => [
                    [
                        'label' => 'Explorar oportunidades',
                        'url' => '/oportunidades',
                        'type' => 'primary',
                    ],
                    [
                        'label' => 'Ver incentivos',
                        'url' => '/incentivos',
                        'type' => 'secondary',
                    ],
                    [
                        'label' => 'Manifestar interesse',
                        'url' => '/manifestar-interesse',
                        'type' => 'primary',
                    ],
                    [
                        'label' => 'Área do investidor',
                        'url' => '/login',
                        'type' => 'secondary',
                    ],
                ],
                'sectors' => [
                    [
                        'id' => 1,
                        'name' => 'Agronegócio',
                        'slug' => 'agronegocio',
                        'icon_key' => 'agronegocio',
                        'description' => 'Oportunidades em produção, processamento, logística agrícola e cadeias de valor alimentares.',
                        'opportunities_count' => 12,
                    ],
                    [
                        'id' => 2,
                        'name' => 'Energia',
                        'slug' => 'energia',
                        'icon_key' => 'energia',
                        'description' => 'Projectos ligados a energias renováveis, electrificação produtiva e soluções energéticas para indústria.',
                        'opportunities_count' => 8,
                    ],
                    [
                        'id' => 3,
                        'name' => 'Turismo',
                        'slug' => 'turismo',
                        'icon_key' => 'turismo',
                        'description' => 'Investimentos em hotelaria, ecoturismo, destinos costeiros e infraestruturas turísticas.',
                        'opportunities_count' => 6,
                    ],
                    [
                        'id' => 4,
                        'name' => 'Infraestruturas',
                        'slug' => 'infraestruturas',
                        'icon_key' => 'infraestruturas',
                        'description' => 'Oportunidades em logística, transportes, construção, parques industriais e serviços urbanos.',
                        'opportunities_count' => 10,
                    ],
                ],
                'featured_opportunities' => [
                    [
                        'id' => 1,
                        'reference' => 'INV-MZ-AGR-001',
                        'title' => 'Unidade de processamento agrícola em Nampula',
                        'slug' => 'unidade-processamento-agricola-nampula',
                        'summary' => 'Projecto orientado para processamento de produtos agrícolas e ligação a cadeias de valor regionais.',
                        'sector' => 'Agronegócio',
                        'province' => 'Nampula',
                        'district' => 'Nacala',
                        'maturity' => 'Projecto estruturado',
                        'estimated_investment' => [
                            'amount' => 2500000,
                            'currency' => 'USD',
                        ],
                        'financing_type' => 'Capital e parceria técnica',
                        'impact' => 'Emprego local, aumento da capacidade de processamento e substituição de importações.',
                        'is_featured' => true,
                        'updated_at' => now
()-
>subDays(
3)-
>
toISOString(
),
                    ],
                    [
                        'id' => 2,
                        'reference' => 'INV-MZ-ENE-002',
                        'title' => 'Sistema solar para agroindústria',
                        'slug' => 'sistema-solar-agroindustria',
                        'summary' => 'Oportunidade para investimento em energia renovável aplicada a unidades produtivas.',
                        'sector' => 'Energia',
                        'province' => 'Manica',
                        'district' => null,
                        'maturity' => 'Pré-estudo disponível',
                        'estimated_investment' => [
                            'amount' => 850000,
                            'currency' => 'USD',
                        ],
                        'financing_type' => 'Crédito, leasing ou PPP',
                        'impact' => 'Redução de custos energéticos e maior estabilidade operacional.',
                        'is_featured' => true,
                        'updated_at' => now
()-
>subDays(
5)-
>
toISOString(
),
                    ],
                    [
                        'id' => 3,
                        'reference' => 'INV-MZ-TUR-003',
                        'title' => 'Eco-lodge costeiro com componente comunitária',
                        'slug' => 'eco-lodge-costeiro-componente-comunitaria',
                        'summary' => 'Projecto turístico com foco em alojamento sustentável, emprego local e valorização ambiental.',
                        'sector' => 'Turismo',
                        'province' => 'Inhambane',
                        'district' => 'Vilankulo',
                        'maturity' => 'Estudo de viabilidade em curso',
                        'estimated_investment' => [
                            'amount' => 1200000,
                            'currency' => 'USD',
                        ],
                        'financing_type' => 'Capital, parceria estratégica ou concessão',
                        'impact' => 'Turismo sustentável, emprego jovem e dinamização de fornecedores locais.',
                        'is_featured' => true,
                        'updated_at' => now
()-
>subDays(
8)-
>
toISOString(
),
                    ],
                ],
                'latest_opportunities' => [
                    [
                        'id' => 4,
                        'reference' => 'INV-MZ-INF-004',
                        'title' => 'Centro logístico provincial',
                        'slug' => 'centro-logistico-provincial',
                        'summary' => 'Infraestrutura para armazenamento, distribuição e apoio à comercialização de produtos locais.',
                        'sector' => 'Infraestruturas',
                        'province' => 'Sofala',
                        'district' => 'Beira',
                        'maturity' => 'Ideia identificada',
                        'estimated_investment' => [
                            'amount' => 3000000,
                            'currency' => 'USD',
                        ],
                        'financing_type' => 'PPP ou financiamento misto',
                        'impact' => 'Melhoria logística, redução de perdas e integração de cadeias produtivas.',
                        'is_featured' => false,
                        'updated_at' => now
()-
>subDays(
1)-
>
toISOString(
),
                    ],
                    [
                        'id' => 5,
                        'reference' => 'INV-MZ-BLUE-005',
                        'title' => 'Processamento de pescado e cadeia fria',
                        'slug' => 'processamento-pescado-cadeia-fria',
                        'summary' => 'Projecto para reforçar conservação, processamento e distribuição de pescado.',
                        'sector' => 'Economia Azul',
                        'province' => 'Zambézia',
                        'district' => 'Quelimane',
                        'maturity' => 'Pré-estudo disponível',
                        'estimated_investment' => [
                            'amount' => 900000,
                            'currency' => 'USD',
                        ],
                        'financing_type' => 'Capital, crédito ou parceria operacional',
                        'impact' => 'Redução de perdas pós-captura e aumento de rendimento dos operadores locais.',
                        'is_featured' => false,
                        'updated_at' => now
()-
>subDays(
2)-
>
toISOString(
),
                    ],
                ],
                'investor_journey' => [
                    [
                        'order' => 1,
                        'title' => 'Identificar oportunidade',
                        'description' => 'Pesquisar sectores, províncias e projectos compatíveis com o perfil do investidor.',
                    ],
                    [
                        'order' => 2,
                        'title' => 'Consultar requisitos',
                        'description' => 'Verificar licenças, incentivos, enquadramento fiscal, cambial e ambiental.',
                    ],
                    [
                        'order' => 3,
                        'title' => 'Manifestar interesse',
                        'description' => 'Enviar dados iniciais para contacto, triagem e orientação especializada.',
                    ],
                    [
                        'order' => 4,
                        'title' => 'Preparar dossier',
                        'description' => 'Organizar plano de negócios, documentos legais, técnicos e financeiros.',
                    ],
                    [
                        'order' => 5,
                        'title' => 'Submeter e acompanhar',
                        'description' => 'Registar o pedido e acompanhar o estado de análise ou validação.',
                    ],
                ],
                'legal_highlights' => [
                    [
                        'title' => 'Guia de incentivos fiscais',
                        'category' => 'Fiscalidade',
                        'url' => '/recursos/guia-incentivos-fiscais',
                    ],
                    [
                        'title' => 'Registo de investimento estrangeiro',
                        'category' => 'Regime cambial',
                        'url' => '/recursos/registo-investimento-estrangeiro',
                    ],
                    [
                        'title' => 'Zonas Económicas Especiais e Zonas Francas Industriais',
                        'category' => 'Incentivos territoriais',
                        'url' => '/recursos/zee-zfi',
                    ],
                ],
                'stats' => [
                    'opportunities' => 48,
                    'sectors' => 6,
                    'provinces' => 11,
                    'interest_requests' => 120,
                    'projects_in_review' => 18,
                ],
            ],
        ]);
    }
}
Rota para testar em routes/api.php
<?php
use App\Http\Controllers\Api\V1\HomeController;
use Illuminate\Support\Facades\Route;
Route::
prefix('v1
')-
>
group(
function () {
    
Route::get(
'/home', [
HomeController::
class, 'show'])
        ->middleware('throttle:60,1');
});
Exemplo de chamada no navegador
import 
{ getHomeData
 } from 
"..
/services/homeService";
const home = await getHomeData("pt");
Segue uma versão 
corrigida, completa e copiável
 do que pediu: HomePage.jsx com integração, incluindo SectorGrid e PortalStats, mais um exemplo de uso do HomeController.php.
1. src/pages/HomePage.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
import HeroSection from 
"..
/components/home/HeroSection";
import SectorGrid from 
"..
/components/sectors/SectorGrid";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
export default function 
HomePage(
) {
  const [home, setHome] = useState(null);
  const [locale, setLocale] = useState("pt");
  const [searchTerm, setSearchTerm] = 
useState(
"");
  const [loading, setLoading] = useState(true);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    let active = true;
    async function 
loadHome(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await getHomeData(locale);
        if 
(!active
) return;
        
setHome(
response?.data || response);
      } catch (err) {
        if 
(!active
) return;
        
setError(
          
err?.
message ||
            "Não foi possível carregar os dados da página principal."
        );
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }
    
loadHome(
);
    return () => {
      active = false;
    };
  }, [locale]);
  function handleSearchSubmit(event) {
    
event.preventDefault
();
    const query = searchTerm.trim();
    if 
(!query
) return;
    
window.location
.href = `/oportunidades?query=${encodeURIComponent(query
)}`
;
  }
  if (loading) {
    return (
      <main className="min-h-screen flex items-center justify-center bg-gray-50">
        <div className="text-center">
          <div className="w-12 h-12 mx-auto border-4 border-blue-200 border-t-blue-900 rounded-full animate-spin" />
          <p className="mt-4 text-gray-600">
            A carregar o Portal do Investidor...
          </p>
        </div>
      </main>
    );
  }
  if (error) {
    return (
      <main className="min-h-screen flex items-center justify-center bg-gray-50 px-6">
        <div className="max-w-lg bg-white border border-red-200 rounded-2xl p-8 shadow-sm text-center">
          <h1 className="text-2xl font-bold text-red-700">
            Erro ao carregar a página
          </h1>
          <p className="mt-3 text-gray-600">{error}</p>
          <button
            type="button"
            onClick
={
() => 
window.location
.
reload(
)}
            className="mt-6 px-5 py-3 rounded-lg bg-blue-950 text-white font-semibold hover:bg-blue-800"
          >
            Tentar novamente
          </button>
        </div>
      </main>
    );
  }
  if 
(!home
) return null;
  const {
    hero = {},
    quickActions = [],
    searchSuggestions = [],
    sectors = [],
    featuredOpportunities = [],
    investorJourney = [],
    incentivesPreview = [],
    legalHighlights = [],
    partnershipExchange = null,
    stats = {},
  } = home;
  return (
    <main className="min-h-screen bg-gray-50 text-gray-900">
      <Header locale={locale} setLocale={setLocale} />
      <HeroSection
        hero={hero}
        searchSuggestions={searchSuggestions}
        searchTerm={searchTerm}
        setSearchTerm={setSearchTerm}
        onSearchSubmit={handleSearchSubmit}
      />
      {quickActions.length > 0 && (
        <section className="max-w-7xl mx-auto px-6 py-10">
          <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
            {quickActions.map((action) => (
              <a
                key={
action.label
}
                href={action.url}
                className="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md hover:border-blue-200 transition"
              >
                <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                  Acesso rápido
                </span>
                <h3 className="mt-2 font-bold text-blue-950">
                  {
action.label
}
                </h3>
                <p className="mt-2 text-sm text-gray-500">Aceder agora</p>
              </a>
            ))}
          </div>
        </section>
      )}
      <SectorGrid initialSectors={sectors} />
      <FeaturedOpportunities initialOpportunities={featuredOpportunities} />
      {investorJourney.length > 0 && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="mb-8">
            <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
              Guia do investidor
            </span>
            <h2 className="mt-2 text-3xl font-bold text-blue-950">
              Como investir em Moçambique
            </h2>
            <p className="mt-3 text-gray-600 max-w-3xl">
              Siga um percurso simples para identificar oportunidades,
              compreender requisitos e iniciar contacto.
            </p>
          </div>
          <div className="grid md:grid-cols-3 gap-6">
            {investorJourney.map((step) => (
              <article
                key={
step.step
}
                className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm"
              >
                <div className="w-11 h-11 rounded-full bg-blue-950 text-white flex items-center justify-center font-bold">
                  {
step.step
}
                </div>
                <h3 className="mt-5 text-xl font-bold text-blue-950">
                  {
step.title
}
                </h3>
                <p className="mt-3 text-gray-600 leading-relaxed">
                  {
step.description
}
                </p>
              </article>
            ))}
          </div>
        </section>
      )}
      {(incentivesPreview.length > 0 || legalHighlights.length > 0) && (
        <section className="bg-blue-50 py-12">
          <div className="max-w-7xl mx-auto px-6 grid lg:grid-cols-2 gap-8">
            <div>
              <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                Incentivos
              </span>
              <h2 className="mt-2 text-3xl font-bold text-blue-950">
                Incentivos e benefícios
              </h2>
              <div className="mt-6 space-y-4">
                {incentivesPreview.map((item) => (
                  <a
                    key={
item.title
}
                    href={item.url}
                    className="block bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition"
                  >
                    <h3 className="font-bold text-blue-950">{
item.title
}</h3>
                    <p className="mt-2 text-gray-600">{
item.description
}</p>
                  </a>
                ))}
              </div>
            </div>
            <div>
              <span className="text-sm uppercase tracking-wide text-blue-700 font-semibold">
                Recursos legais
              </span>
              <h2 className="mt-2 text-3xl font-bold text-blue-950">
                Documentos em destaque
              </h2>
              <div className="mt-6 space-y-4">
                {legalHighlights.map((item) => (
                  <a
                    key={
item.title
}
                    href={item.url}
                    className="block bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition"
                  >
                    <span className="text-sm text-gray-500">
                      {
item.category
}
                    </span>
                    <h3 className="mt-1 font-bold text-blue-950">
                      {
item.title
}
                    </h3>
                  </a>
                ))}
              </div>
            </div>
          </div>
        </section>
      )}
      {partnershipExchange && (
        <section className="max-w-7xl mx-auto px-6 py-12">
          <div className="bg-blue-950 text-white rounded-2xl p-8 md:p-10">
            <span className="text-sm uppercase tracking-wide text-green-300 font-semibold">
              Parcerias e capital
            </span>
            <h2 className="mt-3 text-3xl font-bold">
              {partnershipExchange.title}
            </h2>
            <p className="mt-3 text-blue-100 max-w-3xl">
              {partnershipExchange.description}
            </p>
            <div className="mt-6 flex flex-wrap gap-4">
              {
partnershipExchange.actions?.
map((action) => (
                <a
                  key={
action.label
}
                  href={action.url}
                  className="inline-flex items-center justify-center px-5 py-3 rounded-lg bg-white text-blue-950 font-semibold hover:bg-blue-50"
                >
                  {
action.label
}
                </a>
              ))}
            </div>
          </div>
        </section>
      )}
      <PortalStats stats={stats} />
      <Footer />
    </main>
  );
}
function 
Header({ locale
, 
setLocale }
) {
  return (
    <header className="bg-white border-b border-gray-200 sticky top-0 z-40">
      <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-6">
        <a href="/" className="font-black text-blue-950 text-xl">
          INVESTE Moçambique
        </a>
        <nav className="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-700">
          <a href="/oportunidades" className="hover:text-blue-900">
            Oportunidades
          </a>
          <a href="/sectores" className="hover:text-blue-900">
            Sectores
          </a>
          <a href="/guia-do-investidor" className="hover:text-blue-900">
            Como investir
          </a>
          <a href="/incentivos" className="hover:text-blue-900">
            Incentivos
          </a>
          <a href="/recursos" className="hover:text-blue-900">
            Recursos legais
          </a>
        </nav>
        <div className="flex items-center gap-3">
          <button
            type="button"
            onClick
={
() => setLocale("pt")}
            className
={
              locale === "pt"
                ? "font-bold text-blue-950"
                : "text-gray-500 hover:text-blue-950"
            }
          >
            PT
          </button>
          <span className="text-gray-300">|</span>
          <button
            type="button"
            onClick
={
() => setLocale("en")}
            className
={
              locale === "en"
                ? "font-bold text-blue-950"
                : "text-gray-500 hover:text-blue-950"
            }
          >
            EN
          </button>
          <a
            href="/login"
            className="hidden sm:inline-flex px-4 py-2 rounded-lg bg-blue-950 text-white text-sm font-semibold hover:bg-blue-800"
          >
            Área do investidor
          </a>
        </div>
      </div>
    </header>
  );
}
function 
PortalStats({ stats
 }) {
  const items = [
    {
      label: "Oportunidades",
      value: 
stats?.opportunities ??
 0,
    },
    {
      label: "Sectores",
      value: 
stats?.sectors ??
 0,
    },
    {
      label: "Províncias",
      value: 
stats?.provinces ??
 0,
    },
    {
      label: "Pedidos recebidos",
      value: 
stats?.interestRequests ??
 0,
    },
  ];
  return (
    <section className="max-w-7xl mx-auto px-6 py-12">
      <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
        {items.map((item) => (
          <div
            key={
item.label
}
            className="bg-white border border-gray-200 rounded-2xl p-6 text-center shadow-sm"
          >
            <p className="text-3xl font-black text-blue-950">
              {Number(item.value
).toLocaleString
("pt-MZ")}
            </p>
            <p className="mt-2 text-gray-600">{
item.label
}</p>
          </div>
        ))}
      </div>
      {
stats?.
updatedAt && (
        <p className="mt-4 text-sm text-gray-500 text-center">
          Dados actualizados em {
stats.updatedAt
}
        </p>
      )}
    </section>
  );
}
function 
Footer(
) {
  return (
    <footer className="bg-gray-950 text-white py-10">
      <div className="max-w-7xl mx-auto px-6 flex flex-col lg:flex-row justify-between gap-8">
        <div>
          <h2 className="font-black text-xl">INVESTE Moçambique</h2>
          <p className="mt-3 text-gray-300 max-w-md">
            Plataforma de informação, orientação e facilitação de oportunidades
            de investimento em Moçambique.
          </p>
        </div>
        <div className="grid sm:grid-cols-3 gap-8 text-sm">
          <div>
            <h3 className="font-bold mb-3">Portal</h3>
            <ul className="space-y-2 text-gray-300">
              <li>
                <a href="/oportunidades" className="hover:text-white">
                  Oportunidades
                </a>
              </li>
              <li>
                <a href="/sectores" className="hover:text-white">
                  Sectores
                </a>
              </li>
              <li>
                <a href="/incentivos" className="hover:text-white">
                  Incentivos
                </a>
              </li>
            </ul>
          </div>
          <div>
            <h3 className="font-bold mb-3">Apoio</h3>
            <ul className="space-y-2 text-gray-300">
              <li>
                <a href="/guia-do-investidor" className="hover:text-white">
                  Guia do investidor
                </a>
              </li>
              <li>
                <a href="/recursos" className="hover:text-white">
                  Recursos legais
                </a>
              </li>
              <li>
                <a href="/contactos" className="hover:text-white">
                  Contactos
                </a>
              </li>
            </ul>
          </div>
          <div>
            <h3 className="font-bold mb-3">Legal</h3>
            <ul className="space-y-2 text-gray-300">
              <li>
                <a href="/termos" className="hover:text-white">
                  Termos de uso
                </a>
              </li>
              <li>
                <a href="/privacidade" className="hover:text-white">
                  Política de privacidade
                </a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </footer>
  );
}
2. Exemplo de uso do HomeController.php
Este controller fornece o payload consumido pela HomePage.jsx
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
class HomeController extends Controller
{
    public function 
show(
Request $request): JsonResponse
    {
        $validated = $request->
validate(
[
            'locale' => ['nullable', 'string', 
Rule::in(
['pt', 'en'])],
        ]);
        $locale = $validated['locale'
] ??
 'pt';
        $payload = 
Cache::remember(
            "portal_home_payload_{$locale}",
            now
()-
>
addMinutes(
15),
            function () use ($locale) {
                return [
                    'hero' => [
                        'title' => $locale === 'pt'
                            ? 'Invista em Moçambique com informação clara e oportunidades estruturadas'
                            : 'Invest in Mozambique with clear information and structured opportunities',
                        'subtitle' => $locale === 'pt'
                            ? 'Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas.'
                            : 'Explore strategic sectors, incentives, legal requirements and validated investment opportunities.',
                        'primaryButton' => [
                            'label' => $locale === 'pt'
                                ? 'Explorar oportunidades'
                                : 'Explore opportunities',
                            'url' => '/oportunidades',
                        ],
                        'secondaryButton' => [
                            'label' => $locale === 'pt'
                                ? 'Como investir'
                                : 'How to invest',
                            'url' => '/guia-do-investidor',
                        ],
                    ],
                    'quickActions' => [
                        [
                            'label' => $locale === 'pt'
                                ? 'Explorar oportunidades'
                                : 'Explore opportunities',
                            'url' => '/oportunidades',
                            'type' => 'primary',
                        ],
                        [
                            'label' => $locale === 'pt'
                                ? 'Ver incentivos'
                                : 'View incentives',
                            'url' => '/incentivos',
                            'type' => 'secondary',
                        ],
                        [
                            'label' => $locale === 'pt'
                                ? 'Manifestar interesse'
                                : 'Submit interest',
                            'url' => '/manifestar-interesse',
                            'type' => 'secondary',
                        ],
                        [
                            'label' => $locale === 'pt'
                                ? 'Área do investidor'
                                : 'Investor area',
                            'url' => '/login',
                            'type' => 'outline',
                        ],
                    ],
                    'searchSuggestions' => [
                        'registo na APIEX',
                        'benefícios fiscais',
                        'Zonas Económicas Especiais',
                        'repatriamento de dividendos',
                        'NUIT',
                        'licenciamento simplificado',
                    ],
                    'sectors' => [
                        [
                            'id' => 1,
                            'name' => 'Agronegócio',
                            'slug' => 'agronegocio',
                            'shortDescription' => 'Produção, agroprocessamento, irrigação e cadeias de valor agrícolas.',
                            'description' => 'Sector estratégico para segurança alimentar, exportações e desenvolvimento rural.',
                            'icon' => 'AGR',
                            'opportunitiesCount' => 12,
                            'url' => '/sectores/agronegocio',
                        ],
                        [
                            'id' => 2,
                            'name' => 'Energia e Mineração',
                            'slug' => 'energia-e-mineracao',
                            'shortDescription' => 'Energia renovável, mineração responsável e infraestruturas associadas.',
                            'description' => 'Área prioritária para transição energética, industrialização e aproveitamento sustentável dos recursos naturais.',
                            'icon' => 'ENE',
                            'opportunitiesCount' => 9,
                            'url' => '/sectores/energia-e-mineracao',
                        ],
                        [
                            'id' => 3,
                            'name' => 'Turismo',
                            'slug' => 'turismo',
                            'shortDescription' => 'Turismo costeiro, ecológico, cultural, hoteleiro e de lazer.',
                            'description' => 'Sector com potencial para emprego, receitas externas e desenvolvimento territorial.',
                            'icon' => 'TUR',
                            'opportunitiesCount' => 7,
                            'url' => '/sectores/turismo',
                        ],
                    ],
                    'featuredOpportunities' => [
                        [
                            'id' => 1,
                            'reference' => 'INV-MZ-AGR-001',
                            'title' => 'Unidade de processamento agrícola em Nampula',
                            'slug' => 'inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula',
                            'summary' => 'Projecto orientado para processamento, embalagem e valorização de produtos agrícolas locais.',
                            'sector' => [
                                'name' => 'Agronegócio',
                                'slug' => 'agronegocio',
                            ],
                            'province' => 'Nampula',
                            'district' => 'Nampula',
                            'maturity' => [
                                'level' => 4,
                                'label' => 'Projecto estruturado',
                            ],
                            'estimatedInvestment' => [
                                'amount' => 2500000,
                                'currency' => 'USD',
                            ],
                            'financingModality' => 'capital',
                            'expectedImpact' => 'Geração de emprego local, substituição de importações e valorização da produção agrícola.',
                            'isFeatured' => true,
                            'publishedAt' => now
()-
>
toDateString(
),
                            'url' => '/oportunidades/inv-mz-agr-001-unidade-de-processamento-agricola-em-nampula',
                        ],
                    ],
                    'investorJourney' => [
                        [
                            'step' => 1,
                            'title' => $locale === 'pt'
                                ? 'Identificar oportunidade'
                                : 'Identify an opportunity',
                            'description' => $locale === 'pt'
                                ? 'Pesquisar sectores, províncias, oportunidades e requisitos iniciais.'
                                : 'Search sectors, provinces, opportunities and initial requirements.',
                        ],
                        [
                            'step' => 2,
                            'title' => $locale === 'pt'
                                ? 'Analisar requisitos'
                                : 'Review requirements',
                            'description' => $locale === 'pt'
                                ? 'Consultar requisitos legais, fiscais, cambiais e sectoriais.'
                                : 'Review legal, tax, foreign exchange and sector requirements.',
                        ],
                        [
                            'step' => 3,
                            'title' => $locale === 'pt'
                                ? 'Manifestar interesse'
                                : 'Submit interest',
                            'description' => $locale === 'pt'
                                ? 'Submeter contacto inicial ou pedido de informação ao portal.'
                                : 'Submit an initial contact or information request through the portal.',
                        ],
                    ],
                    'incentivesPreview' => [
                        [
                            'title' => 'Benefícios fiscais',
                            'description' => 'Informação orientadora sobre benefícios fiscais aplicáveis por sector e localização.',
                            'url' => '/incentivos/beneficios-fiscais',
                        ],
                        [
                            'title' => 'ZEE e ZFI',
                            'description' => 'Resumo dos regimes especiais aplicáveis a Zonas Económicas Especiais e Zonas Francas Industriais.',
                            'url' => '/incentivos/zee-zfi',
                        ],
                    ],
                    'legalHighlights' => [
                        [
                            'title' => 'Guia de investimento privado',
                            'category' => 'Investimento',
                            'url' => '/recursos/guia-investimento-privado',
                        ],
                        [
                            'title' => 'Guia de repatriamento de capitais',
                            'category' => 'Regulação cambial',
                            'url' => '/recursos/repatriamento-capitais',
                        ],
                    ],
                    'partnershipExchange' => [
                        'title' => 'Bolsa de Parcerias, Investidores e Capital',
                        'description' => 'Ligue-se a promotores, investidores, empresas e parceiros institucionais.',
                        'actions' => [
                            [
                                'label' => 'Procurar parceiro',
                                'url' => '/parcerias',
                            ],
                            [
                                'label' => 'Submeter projecto',
                                'url' => '/submeter-projecto',
                            ],
                        ],
                    ],
                    'stats' => [
                        'opportunities' => 48,
                        'sectors' => 6,
                        'provinces' => 11,
                        'interestRequests' => 120,
                        'updatedAt' => now
()-
>
toDateString(
),
                    ],
                ];
            }
        );
        return 
response(
)->
json(
[
            'success' => true,
            'locale' => $locale,
            'data' => $payload,
        ]);
    }
}
3. Rota necessária em routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\HomeController;
Route::
prefix('v1')
    ->middleware(['throttle:api'])
    ->
group(
function () {
        
Route::get(
'/home', [
HomeController::
class, 'show']);
    });
4. Serviço React necessário — src/services/homeService.js
import axios from "axios";
const API_BASE_URL = 
import.meta.env.VITE
_API_URL || "http://localhost:8000";
const apiClient = 
axios.create
({
  baseURL: `${API_BASE_
URL}/
api/v1`,
  timeout: 15000,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    const message =
      error
?.response?.data?.
message ||
      
error?.
message ||
      "Ocorreu um erro ao comunicar com a API.";
    return Promise.reject({
      message,
      status: error
?.response?.status
 || 500,
      errors: error
?.response?.data?.
errors || null,
    });
  }
);
export async function 
getHomeData(
locale = "pt") {
  return 
apiClient.get(
"/home", {
    params: 
{ locale
 },
  });
}
export default {
  getHomeData,
};
Com estes ficheiros, a home já consegue:
carregar dados da API Laravel;
alternar entre pt e en;
renderizar HeroSection;
renderizar SectorGrid;
renderizar oportunidades em destaque;
apresentar o percurso do investidor;
mostrar incentivos, documentos legais, bolsa de parcerias e PortalStats.
1. Como integrar HomePage.jsx no projecto React
Estrutura recomendada
src/
  components/
    home/
      HeroSection.jsx
      PortalStats.jsx
      InvestorJourney.jsx
    layout/
      Header.jsx
      Footer.jsx
    opportunities/
      FeaturedOpportunities.jsx
      LatestOpportunities.jsx
      OpportunityCard.jsx
    sectors/
      SectorGrid.jsx
      SectorCard.jsx
  pages/
    HomePage.jsx
  services/
    homeService.js
  routes/
    AppRoutes.jsx
  App.jsx
  main.jsx
Passo 1 — colocar a página
Crie o ficheiro:
src/pages/HomePage.jsx
import 
{ useEffect
, 
useState }
 from "react";
import 
{ getHomeData
 } from 
"..
/services/homeService";
import Header from 
"..
/components/layout/Header";
import Footer from 
"..
/components/layout/Footer";
import FeaturedOpportunities from 
"..
/components/opportunities/FeaturedOpportunities";
import LatestOpportunities from 
"..
/components/opportunities/LatestOpportunities";
Se ainda não tiver os outros componentes prontos, pode começar apenas com FeaturedOpportunities e LatestOpportunities.
Passo 2 — configurar a rota com React Router
Instale o React Router, se ainda não estiver instalado:
npm install react-router-dom
Crie ou edite:
src/routes/AppRoutes.jsx
import 
{ BrowserRouter
, Routes, 
Route }
 from "react-router-dom";
import HomePage from 
"..
/pages/HomePage";
export default function 
AppRoutes(
) {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element
={
<HomePage />} />
      </Routes>
    </BrowserRouter>
  );
}
Depois, no App.jsx:
import AppRoutes from 
"./
routes/AppRoutes";
export default function 
App(
) {
  return <AppRoutes />;
}
Passo 3 — configurar URL da API
No 
ficheiro .env
 do React/Vite:
VITE_API_URL=http://localhost:8000
Depois reinicie o servidor React:
npm run dev
2. Como testar a API do HomeController.php
Passo 1 — confirmar a rota Laravel
No ficheiro:
routes/api.php
adicione:
use App\Http\Controllers\Api\V1\HomeController;
use Illuminate\Support\Facades\Route;
Route::
prefix('v1
')-
>
group(
function () {
    
Route::get(
'/home', [
HomeController::
class, 'show'])
        ->middleware('throttle:60,1');
});
Passo 2 — iniciar o servidor Laravel
php artisan serve
Normalmente ficará disponível em:
http://localhost:8000
Passo 3 — testar no navegador
Abra:
http://localhost:8000/api/v1/home?locale=pt
Se estiver correcto, deve receber JSON com:
{
  "status": "success",
  "locale": "pt",
  "data": {
    "hero": {},
    "sectors": [],
    "featured_opportunities": [],
    "latest_opportunities": [],
    "stats": {}
  }
}
Passo 4 — testar com curl
curl -X GET "http://localhost:8000/api/v1/home?locale=pt" \
  -H "Accept: application/json"
Passo 5 — testar com Postman ou Insomnia
Use:
GET http://localhost:8000/api/v1/home?locale=pt
Headers:
Accept: application/json
Content-Type: application/json
Passo 6 — confirmar se a rota existe
php artisan route:list
Procure uma linha semelhante a:
GET|HEAD api/v1/home App\Http\Controllers\Api\V1\HomeController@show
3. Sugestões de melhoria para o layout da HomePage.jsx
1. Hero mais forte e orientado à acção
A primeira dobra da página deve responder rapidamente a três perguntas:
O que é a plataforma?
Que valor entrega ao investidor?
Qual é o próximo passo?
Sugestão de botões:
Explorar oportunidades
Manifestar interesse
Como investir em Moçambique
2. Barra de pesquisa logo abaixo do Hero
A pesquisa deve aparecer antes dos sectores, com sugestões como:
benefícios fiscais
APIEX
ZEE
agronegócio
energia solar
repatriamento de dividendos
Isto transforma a home numa ferramenta prática, não apenas institucional.
3. Secção de sectores com ícones reais
Use cards por sector com:
ícone;
nome do sector;
breve descrição;
número de oportunidades;
botão “Ver oportunidades”.
Exemplo visual:
Agronegócio
12 oportunidades
Produção, processamento e cadeias de valor agrícolas.
[Ver oportunidades]
4. Separar “destaques” de “recentes”
Mantenha duas secções distintas:
Oportunidades em destaque
Para projectos prioritários.
Oportunidades mais recentes
Para projectos novos ou actualizados.
Isto ajuda o utilizador a perceber diferença entre prioridade editorial e actualização cronológica.
5. Inserir bloco “Como investir” antes do footer
Sugestão de fluxo:
Identificar sector.
Consultar requisitos.
Manifestar interesse.
Preparar dossier.
Acompanhar processo.
Este bloco é importante para investidores estrangeiros e utilizadores que não conhecem o processo moçambicano.
6. Indicadores públicos para credibilidade
Inclua um bloco PortalStats com:
48 oportunidades
6 sectores
11 províncias
120 manifestações de interesse
18 projectos em análise
Mesmo que os números sejam inicialmente mockados, a estrutura já prepara a plataforma para dados reais.
7. Melhorias de UX
Recomendo:
layout responsivo com cards em 1 coluna no mobile, 2 no tablet e 3 no desktop;
botões com contraste forte;
textos curtos nos cards;
estados de loading com skeletons;
mensagem clara quando não houver oportunidades;
chamada fixa ou repetida para “Manifestar interesse”;
suporte desde o início para locale=pt e locale=en.
4. Ordem prática de implementação
Eu seguiria esta ordem:
Testar GET /api/v1/home?locale=pt no Laravel.
Confirmar se o JSON chega correctamente.
Criar homeService.js.
Criar HomePage.jsx.
Ligar FeaturedOpportunities e LatestOpportunities.
Adicionar SectorGrid.
Adicionar HeroSection.
Melhorar visual com Tailwind.
Só depois ligar formulários e área reservada.
Se quiser, posso no próximo passo entregar o 
HomePage.jsx completo
, já integrado com homeService.js, FeaturedOpportunities, LatestOpportunities, estados de loading e tratamento de erro.
S
egue uma implementação 
completa e copiável
 para melhorar a HomePage com 
CSS próprio
, 
layout mobile-first
, 
acessibilidade
 e 
testes unitários React
.
1. src/styles/home.css
:root {
  --color-primary: #0b1f4d;
  --color-primary-light: #123a7a;
  --color-secondary: #16a34a;
  --color-secondary-dark: #15803d;
  --color-bg: #f8fafc;
  --color-surface: #ffffff;
  --color-muted: #64748b;
  --color-text: #0f172a;
  --color-border: #e2e8f0;
  --color-danger: #b91c1c;
  --color-danger-bg: #fef2f2;
  --color-focus: #22c55e;
  --radius-sm: 0.5rem;
  --radius-md: 0.75rem;
  --radius-lg: 1rem;
  --radius-xl: 1.25rem;
  --radius-2xl: 1.5rem;
  --shadow-sm: 0 1px 2px 
rgba(
15, 23, 42, 0.06);
  --shadow-md: 0 8px 24px 
rgba(
15, 23, 42, 0.08);
  --container: 1180px;
}
* {
  box-sizing: border-box;
}
html {
  scroll-behavior: smooth;
}
body {
  margin: 0;
  background: 
var(
--color-bg);
  color: 
var(
--color-text);
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
}
a {
  color: inherit;
  text-decoration: none;
}
button,
input,
select,
textarea {
  font: inherit;
}
button {
  cursor: pointer;
}
button:disabled {
  cursor: not-allowed;
}
a:focus-visible,
button:focus-visible,
input:focus-visible {
  outline: 3px solid 
var(
--color-focus);
  outline-offset: 3px;
}
.home
-page {
  min-height: 100vh;
  background: 
var(
--color-bg);
  color: 
var(
--color-text);
}
.home
-container {
  max-width: 
var(
--container);
  margin: 0 auto;
  padding: 0 1rem;
}
/* Header */
.site
-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: 
rgba(
255, 255, 255, 0.96);
  border-bottom: 1px solid 
var(
--color-border);
  backdrop-filter: blur(10px);
}
.site
-header__inner {
  min-height: 72px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.site
-header__brand {
  font-weight: 900;
  color: 
var(
--color-primary);
  letter-spacing: -0.02em;
}
.site
-header__nav {
  display: none;
  align-items: center;
  gap: 1.25rem;
  color: #334155;
  font-size: 0.95rem;
  font-weight: 600;
}
.site
-header__nav a:hover {
  color: 
var(
--color-primary);
}
.site
-header__actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.locale
-button {
  border: 0;
  background: transparent;
  color: 
var(
--color-muted);
  font-weight: 700;
  padding: 0.25rem;
}
.locale
-button[aria-pressed="true"] {
  color: 
var(
--color-primary);
}
.login-link {
  display: none;
  background: 
var(
--color-primary);
  color: white;
  padding: 0.65rem 1rem;
  border-radius: 
var(
--radius-md);
  font-weight: 800;
  font-size: 0.9rem;
}
/* Hero */
.hero
 {
  background:
    radial-
gradient(
circle at top right, 
rgba(
34, 197, 94, 0.18), transparent 32%),
    linear-
gradient(
135deg, 
var(
--color-primary), #06122e);
  color: white;
}
.hero
__inner {
  padding: 4rem 1rem;
  display: grid;
  gap: 2rem;
}
.hero
__eyebrow {
  display: inline-flex;
  background: 
rgba(
255, 255, 255, 0.12);
  color: #dbeafe;
  padding: 0.45rem 0.8rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 800;
}
.hero
__title {
  margin: 1.5rem 0 0;
  font-size: 
clamp(
2rem, 7vw, 4rem);
  line-height: 1.05;
  letter-spacing: -0.04em;
}
.hero
__subtitle {
  margin: 1.25rem 0 0;
  color: #dbeafe;
  font-size: 1.05rem;
  line-height: 1.7;
  max-width: 42rem;
}
.hero
__actions {
  margin-top: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.btn
 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.8rem 1.2rem;
  border-radius: 
var(
--radius-md);
  font-weight: 900;
  border: 1px solid transparent;
}
.btn
--primary {
  background: 
var(
--color-secondary);
  color: #052e16;
}
.btn
--primary:hover {
  background: #4ade80;
}
.btn
--secondary {
  background: white;
  color: 
var(
--color-primary);
}
.btn
--outline {
  background: transparent;
  border-color: 
rgba(
255, 255, 255, 0.35);
  color: white;
}
.hero
-search {
  background: 
rgba(
255, 255, 255, 0.1);
  border: 1px solid 
rgba(
255, 255, 255, 0.16);
  border-radius: 
var(
--radius-2xl);
  padding: 1.25rem;
}
.hero
-search__title {
  margin: 0;
  font-size: 1.25rem;
}
.hero
-search__description {
  color: #dbeafe;
  line-height: 1.6;
}
.hero
-search__form {
  margin-top: 1.25rem;
  display: grid;
  gap: 0.75rem;
}
.hero
-search__input {
  width: 100%;
  min-height: 48px;
  border-radius: 
var(
--radius-md);
  border: 1px solid transparent;
  padding: 0 1rem;
}
.hero
-search__button {
  min-height: 48px;
  border-radius: 
var(
--radius-md);
  border: 0;
  background: 
var(
--color-secondary);
  color: #052e16;
  font-weight: 900;
}
.hero
-search__suggestions {
  margin-top: 1rem;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.hero
-search__suggestion {
  border: 0;
  background: 
rgba(
255, 255, 255, 0.16);
  color: white;
  border-radius: 999px;
  padding: 0.4rem 0.75rem;
  font-size: 0.85rem;
}
/* Sections */
.section
 {
  padding: 3rem 0;
}
.section
__header {
  margin-bottom: 2rem;
}
.section
__eyebrow {
  color: #1d4ed8;
  text-transform: uppercase;
  font-size: 0.8rem;
  font-weight: 900;
  letter-spacing: 0.08em;
}
.section
__title {
  margin: 0.5rem 0 0;
  font-size: 
clamp(
1.75rem, 4vw, 2.5rem);
  color: 
var(
--color-primary);
  letter-spacing: -0.03em;
}
.section
__subtitle {
  margin: 0.75rem 0 0;
  color: 
var(
--color-muted);
  line-height: 1.7;
  max-width: 48rem;
}
/* Quick actions */
.quick
-actions {
  display: grid;
  gap: 1rem;
}
.quick
-action {
  background: 
var(
--color-surface);
  border: 1px solid 
var(
--color-border);
  border-radius: 
var(
--radius-xl);
  padding: 1.25rem;
  box-shadow: 
var(
--shadow-sm);
}
.quick
-action:hover {
  border-color: #bfdbfe;
  box-shadow: 
var(
--shadow-md);
}
.quick
-action__label {
  margin: 0.4rem 0 0;
  color: 
var(
--color-primary);
  font-weight: 900;
}
/* Sectors */
.sector
-grid {
  display: grid;
  gap: 1rem;
}
.sector
-card {
  background: 
var(
--color-surface);
  border: 1px solid 
var(
--color-border);
  border-radius: 
var(
--radius-2xl);
  padding: 1.5rem;
  box-shadow: 
var(
--shadow-sm);
  transition: 0.2s ease;
}
.sector
-card:hover {
  border-color: #bfdbfe;
  box-shadow: 
var(
--shadow-md);
  transform: translateY(-2px);
}
.sector
-card__icon {
  width: 48px;
  height: 48px;
  border-radius: 
var(
--radius-lg);
  background: #eff6ff;
  color: 
var(
--color-primary);
  display: grid;
  place-items: center;
  font-weight: 900;
  margin-bottom: 1.25rem;
}
.sector
-card__title {
  margin: 0;
  color: 
var(
--color-primary);
  font-size: 1.25rem;
}
.sector
-card__description {
  color: 
var(
--color-muted);
  line-height: 1.65;
}
.sector
-card__footer {
  margin-top: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.sector
-card__count {
  color: 
var(
--color-muted);
  font-size: 0.9rem;
}
.sector
-card__link {
  color: #1d4ed8;
  font-weight: 900;
}
/* Stats */
.portal
-stats {
  display: grid;
  gap: 1rem;
}
.portal
-stat {
  background: 
var(
--color-surface);
  border: 1px solid 
var(
--color-border);
  border-radius: 
var(
--radius-2xl);
  padding: 1.5rem;
  text-align: center;
  box-shadow: 
var(
--shadow-sm);
}
.portal
-stat__value {
  margin: 0;
  color: 
var(
--color-primary);
  font-size: 2rem;
  font-weight: 1000;
}
.portal
-stat__label {
  margin: 0.5rem 0 0;
  color: 
var(
--color-muted);
}
/* States */
.loading
-screen,
.error
-screen {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 1rem;
}
.spinner
 {
  width: 48px;
  height: 48px;
  border: 4px solid #bfdbfe;
  border-top-color: 
var(
--color-primary);
  border-radius: 999px;
  animation: spin 0.8s linear infinite;
  margin: 0 auto;
}
.error
-card {
  max-width: 520px;
  background: white;
  border: 1px solid #fecaca;
  border-radius: 
var(
--radius-2xl);
  padding: 2rem;
  text-align: center;
}
.error
-card h1 {
  color: 
var(
--color-danger);
}
.error
-card button {
  border: 0;
  background: 
var(
--color-primary);
  color: white;
  min-height: 44px;
  border-radius: 
var(
--radius-md);
  padding: 0.75rem 1.2rem;
  font-weight: 800;
}
/* Accessibility helper */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: 
rect(
0, 0, 0, 0);
}
/* Animations */
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
/* Responsive */
@
media
 (min-width: 640px) {
  
.home
-container {
    padding: 0 1.5rem;
  }
  
.hero
__actions {
    flex-direction: row;
  }
  
.hero
-search__form {
    grid-template-columns: 1fr auto;
  }
  
.quick
-actions {
    grid-template-columns: 
repeat(
2, 1fr);
  }
  
.portal
-stats {
    grid-template-columns: 
repeat(
2, 1fr);
  }
}
@
media
 (min-width: 900px) {
  
.site
-header__nav {
    display: flex;
  }
  .login-link {
    display: inline-flex;
  }
  
.hero
__inner {
    grid-template-columns: 1.1fr 0.9fr;
    padding: 5rem 1.5rem;
  }
  
.sector
-grid {
    grid-template-columns: 
repeat(
3, 1fr);
  }
  
.quick
-actions {
    grid-template-columns: 
repeat(
4, 1fr);
  }
  
.portal
-stats {
    grid-template-columns: 
repeat(
4, 1fr);
  }
}
@
media
 (prefers-reduced-motion: reduce) {
  *,
  
*::
before,
  
*::
after {
    animation: 
none !important
;
    transition: 
none !important
;
    scroll-behavior: 
auto !important
;
  }
}
2. Ajuste recomendado no HomePage.jsx
Importe o CSS no topo:
import "../styles/home.css";
E use classes como:
<main className="home-page">
Para o carregamento:
<main className="loading-screen" aria-live="polite">
  <div>
    <div className="spinner" aria-hidden="true" />
    <p>A carregar o Portal do Investidor...</p>
  </div>
</main>
Para erro:
<main className="error-screen">
  <section className="error-card" role="alert">
    <h1>Erro ao carregar a página</h1>
    <p>{error}</p>
    <button type="button" onClick
={
() => 
window.location
.
reload(
)}>
      Tentar novamente
    </button>
  </section>
</main>
3. Ajustes de acessibilidade no HeroSection.jsx
export default function 
HeroSection(
{
  hero,
  searchSuggestions = [],
  searchTerm,
  setSearchTerm,
  onSearchSubmit,
}) {
  return (
    <section className="hero" aria-labelledby="hero-title">
      <div className="home-container hero__inner">
        <div>
          <span className="hero__eyebrow">INVESTE Moçambique</span>
          <h1 id="hero-title" className="hero__title">
            {
hero?.
title ||
              "Invista em Moçambique com informação clara e oportunidades estruturadas"}
          </h1>
          <p className="hero__subtitle">
            {
hero?.
subtitle ||
              "Explore sectores estratégicos, incentivos, requisitos legais e oportunidades de investimento validadas."}
          </p>
          <div className="hero__actions">
            <a className="btn btn--primary" href
={
hero
?.primaryButton
?.url || "/oportunidades"}>
              {
hero?.primaryButton?.
label || "Explorar oportunidades"}
            </a>
            <a className="btn btn--secondary" href
={
hero
?.secondaryButton
?.url || "/guia-do-investidor"}>
              {
hero?.secondaryButton?.
label || "Como investir"}
            </a>
          </div>
        </div>
        <aside className="hero-search" aria-labelledby="search-title">
          <h2 id="search-title" className="hero-search__title">
            Pesquisa rápida de informação
          </h2>
          <p className="hero-search__description">
            Pesquise oportunidades, incentivos, requisitos legais ou temas relacionados ao investimento.
          </p>
          <form onSubmit={onSearchSubmit} className="hero-search__form" role="search">
            <label className="sr-only" htmlFor="home-search">
              Pesquisar oportunidades, incentivos ou requisitos
            </label>
            <input
              id="home-search"
              className="hero-search__input"
              type="search"
              value={searchTerm}
              onChange={(event) => setSearchTerm(
event.target
.value)}
              placeholder="Ex.: benefícios fiscais, APIEX, ZEE..."
              autoComplete="off"
            />
            <button className="hero-search__button" type="submit">
              Pesquisar
            </button>
          </form>
          {searchSuggestions.length > 0 && (
            <div className="hero-search__suggestions" aria-label="Sugestões de pesquisa">
              {searchSuggestions.map((suggestion) => (
                <button
                  key={suggestion}
                  type="button"
                  className="hero-search__suggestion"
                  onClick
={
() => setSearchTerm(suggestion)}
                >
                  {suggestion}
                </button>
              ))}
            </div>
          )}
        </aside>
      </div>
    </section>
  );
}
4. Ajuste recomendado no SectorCard.jsx
export default function 
SectorCard({ sector
 }) {
  if 
(!sector
) return null;
  const title = sector.name || "Sector";
  const description =
    
sector.shortDescription
 ||
    
sector.short
_description ||
    
sector.description
 ||
    "Informação sectorial em validação.";
  const opportunitiesCount =
    
sector.opportunitiesCount
 ?? 
sector.opportunities
_
count ??
 0;
  const url = sector.url || `/sectores/${
sector.slug
}`;
  return (
    <article className="sector-card" aria-labelledby
={
`sector-${
sector.slug
 || 
sector.id}`
}>
      <div className="sector-card__icon" aria-hidden="true">
        {
sector.icon
 || "INV"}
      </div>
      <h3 id
={
`sector-${
sector.slug
 || 
sector.id}`
} className="sector-card__title">
        {title}
      </h3>
      <p className="sector-card__description">{description}</p>
      <div className="sector-card__footer">
        <span className="sector-card__count">
          {opportunitiesCount
}{
" "}
          {opportunitiesCount === 
1 ?
 "oportunidade
" :
 "oportunidades"}
        </span>
        <a className="sector-card__link" href={url} aria-label
={
`Ver sector ${title}`}>
          Ver sector
        </a>
      </div>
    </article>
  );
}
5. Ajuste recomendado no SectorGrid.jsx
import 
{ useEffect
, 
useState }
 from "react";
import SectorCard from 
"./
SectorCard";
import 
{ getSectors
 } from 
"../..
/services/opportunityService";
export default function 
SectorGrid(
{
  title = "Sectores estratégicos",
  subtitle = "Explore áreas prioritárias de investimento em Moçambique.",
  initialSectors = null,
  showHeader = true,
}) {
  const [sectors, setSectors] = 
useState(
initialSectors || []);
  const [loading, setLoading] = useState
(!initialSectors
);
  const [error, setError] = 
useState(
"");
  
useEffect(
() => {
    if (initialSectors) return;
    let active = true;
    async function 
loadSectors(
) {
      try {
        setLoading(true);
        
setError(
"");
        const response = await 
getSectors(
);
        if 
(!active
) return;
        const data = response?.data || response || [];
        
setSectors(
Array.isArray(data
) ?
 
data :
 []);
      } catch (err) {
        if 
(!active
) return;
        
setError(err?.
message || "Não foi possível carregar os sectores estratégicos.");
      } finally {
        if (active) setLoading(false);
      }
    }
    
loadSectors(
);
    return () => {
      active = false;
    };
  }, [initialSectors]);
  if (loading) {
    return (
      <section className="section" aria-live="polite">
        <div className="home-container">
          <p>A carregar sectores estratégicos...</p>
        </div>
      </section>
    );
  }
  if (error) {
    return (
      <section className="section">
        <div className="home-container">
          <div className="error-card" role="alert">
            <h2>Erro ao carregar sectores</h2>
            <p>{error}</p>
          </div>
        </div>
      </section>
    );
  }
  return (
    <section className="section" aria-labelledby="sectors-title">
      <div className="home-container">
        {showHeader && (
          <header className="section__header">
            <span className="section__eyebrow">Prioridades de investimento</span>
            <h2 id="sectors-title" className="section__title">{title}</h2>
            {subtitle && <p className="section__subtitle">{subtitle}</p>}
          </header>
        )}
        {
sectors.length
 > 
0 ?
 (
          <div className="sector-grid">
            {sectors.map((sector) => (
              <SectorCard
                key
={sector.slug
 || sector.id || sector.name}
                sector={sector}
              />
            ))}
          </div>
        ) : (
          <p>Ainda não existem sectores 
publicados.<
/p>
        )}
      </div>
    </section>
  );
}
6. src/components/home/PortalStats.jsx
export default function 
PortalStats({ stats
 = {
} }
) {
  const items = [
    {
      label: "Oportunidades",
      value: 
stats?.opportunities ??
 0,
    },
    {
      label: "Sectores",
      value: 
stats?.sectors ??
 0,
    },
    {
      label: "Províncias",
      value: 
stats?.provinces ??
 0,
    },
    {
      label: "Pedidos recebidos",
      value: 
stats?.interestRequests ??
 0,
    },
  ];
  return (
    <section className="section" aria-labelledby="portal-stats-title">
      <div className="home-container">
        <header className="section__header">
          <span className="section__eyebrow">Indicadores</span>
          <h2 id="portal-stats-title" className="section__title">
            Dinâmica do portal
          </h2>
        </header>
        <div className="portal-stats">
          {items.map((item) => (
            <article key={
item.label
} className="portal-stat">
              <p className="portal-stat__value">
                {Number(item.value
).toLocaleString
("pt-MZ")}
              </p>
              <p className="portal-stat__label">{
item.label
}</p>
            </article>
          ))}
        </div>
        {
stats?.
updatedAt && (
          <p className="section__subtitle">
            Dados actualizados em {
stats.updatedAt
}
          </p>
        )}
      </div>
    </section>
  );
}
7. Testes unitários React
Instalar dependências
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
vitest.config.js
import 
{ defineConfig
 } from "vitest/config";
import react from "@vitejs/plugin-react";
export default 
defineConfig(
{
  plugins: [
react(
)],
  test: {
    environment: "jsdom",
    setupFiles: "./src/test/setup.js",
  },
});
src/test/setup.js
import "@testing-library/jest-dom";
src/components/sectors/SectorCard.test.jsx
import 
{ render
, 
screen }
 from "@testing-library/react";
import SectorCard from 
"./
SectorCard";
describe(
"SectorCard", () => {
  
it(
"renderiza o nome e descrição do sector", () => {
    
render(
      <SectorCard
        sector
={
{
          name: "Agronegócio",
          slug: "agronegocio",
          shortDescription: "Produção e agroprocessamento.",
          opportunitiesCount: 12,
          icon: "AGR",
        }}
      />
    );
    expect(
screen.getByText
("Agronegócio")
).toBeInTheDocument
();
    
expect(screen.getByText
("Produção e agroprocessamento.")
).toBeInTheDocument
();
    
expect(screen.getByText
("12 oportunidades")
).toBeInTheDocument
();
  });
  
it(
"usa texto alternativo quando dados estão incompletos", () => {
    
render(
<SectorCard sector
={{ slug
: "teste
" }
} />);
    expect(
screen.getByText
("Sector")
).toBeInTheDocument
();
    
expect(screen.getByText
("Informação sectorial em validação.")
).toBeInTheDocument
();
  });
  
it(
"não renderiza nada quando sector é nulo", () => {
    const 
{ container
 } = 
render(
<SectorCard sector={null} />);
    expect(container
).toBeEmptyDOMElement
();
  });
});
src/components/sectors/SectorGrid.test.jsx
import 
{ render
, 
screen }
 from "@testing-library/react";
import SectorGrid from 
"./
SectorGrid";
vi.
mock("../..
/services/opportunityService", () => ({
  getSectors: 
vi.fn
(),
}));
describe(
"SectorGrid", () => {
  
it(
"renderiza sectores recebidos como initialSectors", () => {
    
render(
      <SectorGrid
        initialSectors
={
[
          {
            id: 1,
            name: "Energia",
            slug: "energia",
            shortDescription: "Energia renovável.",
            opportunitiesCount: 9,
          },
        ]}
      />
    );
    
expect(screen.getByText
("Sectores estratégicos")
).toBeInTheDocument
();
    expect(
screen.getByText
("Energia")
).toBeInTheDocument
();
    
expect(screen.getByText
("Energia renovável.")
).toBeInTheDocument
();
  });
  
it(
"mostra mensagem quando não há sectores", () => {
    
render(
<SectorGrid initialSectors
={
[]} />);
    
expect(screen.getByText
("Ainda não existem sectores publicados.")
).toBeInTheDocument
();
  });
});
src/components/home/HeroSection.test.jsx
import 
{ render
, screen, 
fireEvent }
 from "@testing-library/react";
import HeroSection from 
"./
HeroSection";
describe(
"HeroSection", () => {
  
it(
"renderiza título, subtítulo e botões", () => {
    
render(
      <HeroSection
        hero
={
{
          title: "Invista em Moçambique",
          subtitle: "Explore oportunidades estruturadas.",
          primaryButton: {
            label: "Explorar",
            url: "/oportunidades",
          },
          secondaryButton: {
            label: "Como investir",
            url: "/guia",
          },
        }}
        searchSuggestions
={
[]}
        searchTerm=""
        setSearchTerm
={
() => {}}
        onSearchSubmit={(event) => 
event.preventDefault
()}
      />
    );
    
expect(screen.getByText
("Invista em Moçambique")
).toBeInTheDocument
();
    
expect(screen.getByText
("Explore oportunidades estruturadas.")
).toBeInTheDocument
();
    expect(
screen.getByText
("Explorar")
).toBeInTheDocument
();
    
expect(screen.getByText
("Como investir")
).toBeInTheDocument
();
  });
  
it(
"actualiza pesquisa ao clicar numa sugestão", () => {
    const setSearchTerm = 
vi.fn
();
    
render(
      <HeroSection
        hero
={
{}}
        searchSuggestions
={
["benefícios fiscais"]}
        searchTerm=""
        setSearchTerm={setSearchTerm}
        onSearchSubmit={(event) => 
event.preventDefault
()}
      />
    );
    fireEvent.click(
screen.getByText
("benefícios fiscais"));
    expect(setSearchTerm
).toHaveBeenCalledWith
("benefícios fiscais");
  });
});
src/components/home/PortalStats.test.jsx
import 
{ render
, 
screen }
 from "@testing-library/react";
import PortalStats from 
"./
PortalStats";
describe(
"PortalStats", () => {
  
it(
"renderiza os indicadores do portal", () => {
    
render(
      <PortalStats
        stats
={
{
          opportunities: 48,
          sectors: 6,
          provinces: 11,
          interestRequests: 120,
          updatedAt: "2026-08-27",
        }}
      />
    );
    
expect(screen.getByText
("Dinâmica do portal")
).toBeInTheDocument
();
    expect(
screen.getByText
("Oportunidades")
).toBeInTheDocument
();
    expect(
screen.getByText
("Sectores")
).toBeInTheDocument
();
    expect(
screen.getByText
("Províncias")
).toBeInTheDocument
();
    
expect(screen.getByText
("Pedidos recebidos")
).toBeInTheDocument
();
    
expect(screen.getByText
("Dados actualizados em 2026-08-27")
).toBeInTheDocument
();
  });
});
``
8. Adicionar scripts no 
package.json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}
9. Recomendações de acessibilidade aplicadas
Uso de aria-
labelledby
 nas secções principais.
Uso de role="
search
" no formulário de pesquisa.
Uso de role="
alert
" para mensagens de erro.
Campo de pesquisa com 
label
 oculto, mas acessível a leitores de ecrã.
Botões com foco visível.
Suporte
 a 
prefers
-reduced-motion.
Áreas clicáveis com altura mínima confortável.
Estrutura mobile-
first
.
Contraste adequado entre fundo, texto e botões.
10. Próximo passo recomendado
A seguir, recomendo criar:
FeaturedOpportunities.jsx acessível e responsivo;
OpportunityCard.jsx com mobile layout;
testes unitários para oportunidades em destaque;
página /oportunidades com filtros por sector, província, maturidade e modalidade de financiamento.
1. FeaturedOpportunities.jsx acessível
Caminho:
src/components/opportunities/FeaturedOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
FeaturedOpportunities(
{
  title = "Oportunidades em destaque",
  description = "Projectos prioritários com maior relevância estratégica para investidores.",
  opportunities = [],
  viewAllUrl = "/oportunidades",
}) {
  const hasOpportunities = Array.isArray(opportunities) && 
opportunities.length
 > 0;
  return (
    <section
      aria-labelledby="featured-opportunities-title"
      className="bg-slate-50 py-12 sm:py-16"
    >
      <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <p className="mb-2 inline-flex rounded-full bg-blue-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-blue-700">
              Carteira de projectos
            </p>
            <h2
              id="featured-opportunities-title"
              className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl"
            >
              {title}
            </h2>
            {description && (
              <p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600 sm:text-base">
                {description}
              </p>
            )}
          </div>
          {viewAllUrl && (
            <a
              href={viewAllUrl}
              className="inline-flex min-h-11 items-center justify-center rounded-xl border border-blue-700 px-4 py-2 text-sm font-semibold text-blue-700 transition hover:bg-blue-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-700 focus:ring-offset-2"
              aria-label="Ver todas as oportunidades de investimento"
            >
              Ver todas →
            </a>
          )}
        </div>
        
{!hasOpportunities
 ? (
          <div
            role="status"
            aria-live="polite"
            className="rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center"
          >
            <h3 className="text-lg font-semibold text-slate-900">
              Sem oportunidades em destaque
            </h3>
            <p className="mt-2 text-sm text-slate-600">
              Ainda não existem oportunidades em destaque publicadas.
            </p>
          </div>
        ) : (
          <ul
            className="grid list-none gap-6 p-0 md:grid-cols-2 lg:grid-cols-3"
            aria-label="Lista de oportunidades em destaque"
          >
            {opportunities.map((opportunity) => (
              <li key
={
opportunity.id || 
opportunity.reference
 || 
opportunity.slug
}>
                <OpportunityCard opportunity={opportunity} />
              </li>
            ))}
          </ul>
        )}
      </div>
    </section>
  );
}
2. OpportunityCard.jsx optimizado para mobile
Caminho:
src/components/opportunities/OpportunityCard.jsx
function formatInvestment(value) {
  if 
(!value
) return null;
  if (typeof value === "string") return value;
  if (typeof value === "object" && 
value.amount
 && 
value.currency
) {
    return new Intl.NumberFormat("pt-MZ", {
      style: "currency",
      currency: 
value.currency
,
      maximumFractionDigits: 0,
    }
).format
(
value.amount
);
  }
  return null;
}
function getSectorName(sector) {
  if 
(!sector
) return null;
  return typeof sector === "string
" ?
 
sector :
 sector.name;
}
export default function 
OpportunityCard({ opportunity
 }) {
  const title = 
opportunity.title
 || opportunity.name || "Oportunidade de investimento";
  const summary =
    
opportunity.summary
 ||
    
opportunity.description
 ||
    "Informação resumida da oportunidade em validação.";
  const sector = getSectorName(
opportunity.sector
);
  const investment = 
formatInvestment(
    
opportunity.estimated
_investment || 
opportunity.estimatedInvestment
  );
  const financing = 
opportunity.financing
_type || 
opportunity.financingType
;
  const location = [
opportunity.district
, 
opportunity.province].filter
(Boolean
).join
(", ");
  const opportunityUrl = 
opportunity.slug
    ? `/oportunidades/${
opportunity.slug
}`
    : "/oportunidades";
  const interestUrl = 
opportunity.slug
    ? `/manifestar-interesse?opportunity=${
opportunity.slug
}`
    : "/manifestar-interesse";
  return (
    <article className="flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:-translate-y-1 hover:shadow-md sm:p-6">
      <div className="mb-4 flex flex-wrap gap-2">
        {opportunity.is_featured && (
          <span className="rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">
            Destaque
          </span>
        )}
        {sector && (
          <span className="rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">
            {sector}
          </span>
        )}
        {
opportunity.maturity
 && (
          <span className="rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
            {
opportunity.maturity
}
          </span>
        )}
      </div>
      {
opportunity.reference
 && (
        <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
          {
opportunity.reference
}
        </p>
      )}
      <h3 className="mb-3 text-lg font-bold leading-snug text-slate-900">
        <a
          href={opportunityUrl}
          className="rounded focus:outline-none focus:ring-2 focus:ring-blue-700 focus:ring-offset-2"
        >
          {title}
        </a>
      </h3>
      <p className="mb-5 text-sm leading-6 text-slate-600 sm:line-clamp-3">
        {summary}
      </p>
      <dl className="mb-5 space-y-3 text-sm text-slate-600">
        {location && (
          <div className="flex flex-col gap-1 border-b border-slate-100 pb-2 sm:flex-row sm:justify-between">
            <dt className="font-medium text-slate-700">Localização</dt>
            <dd className="text-left sm:text-right">{location}</dd>
          </div>
        )}
        {investment && (
          <div className="flex flex-col gap-1 border-b border-slate-100 pb-2 sm:flex-row sm:justify-between">
            <dt className="font-medium text-slate-700">Investimento</dt>
            <dd className="text-left sm:text-right">{investment}</dd>
          </div>
        )}
        {financing && (
          <div className="flex flex-col gap-1 border-b border-slate-100 pb-2 sm:flex-row sm:justify-between">
            <dt className="font-medium text-slate-700">Financiamento</dt>
            <dd className="text-left sm:text-right">{financing}</dd>
          </div>
        )}
        {
opportunity.impact
 && (
          <div className="flex flex-col gap-1 border-b border-slate-100 pb-2">
            <dt className="font-medium text-slate-700">Impacto</dt>
            <dd>{
opportunity.impact
}</dd>
          </div>
        )}
      </dl>
      {
opportunity.updated
_at && (
        <p className="mb-4 text-xs text-slate-400">
          Actualizado 
em{
" "}
          {new Intl.DateTimeFormat("pt-MZ", {
            day: "2-digit",
            month: "long",
            year: "numeric",
          }
).format
(
new Date
(
opportunity.updated
_at))}
        </p>
      )}
      <div className="mt-auto grid gap-3 sm:grid-cols-2">
        <a
          href={opportunityUrl}
          className="inline-flex min-h-11 items-center justify-center rounded-xl bg-blue-700 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-700 focus:ring-offset-2"
          aria-label
={
`Ver ficha da oportunidade ${title}`}
        >
          Ver ficha
        </a>
        <a
          href={interestUrl}
          className="inline-flex min-h-11 items-center justify-center rounded-xl border border-blue-700 px-4 py-2 text-sm font-semibold text-blue-700 transition hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-700 focus:ring-offset-2"
          aria-label
={
`Manifestar interesse na oportunidade ${title}`}
        >
          Manifestar interesse
        </a>
      </div>
    </article>
  );
}
3. Testes unitários para oportunidades
Instalação recomendada
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
No vite.config.js:
import 
{ defineConfig
 } from "vite";
import react from "@vitejs/plugin-react";
export default 
defineConfig(
{
  plugins: [
react(
)],
  test: {
    environment: "jsdom",
    setupFiles: "./src/test/setup.js",
  },
});
Criar:
src/test/setup.js
import "@testing-library/jest-dom";
OpportunityCard.test.jsx
Caminho:
src/components/opportunities/__tests__/OpportunityCard.test.jsx
import 
{ render
, 
screen }
 from "@testing-library/react";
import OpportunityCard from 
"..
/OpportunityCard";
const opportunity = {
  id: 1,
  reference: "INV-MZ-AGR-001",
  title: "Unidade de processamento agrícola em Nampula",
  slug: "unidade-processamento-agricola-nampula",
  summary: "Projecto orientado para processamento de produtos agrícolas.",
  sector: "Agronegócio",
  province: "Nampula",
  district: "Nacala",
  maturity: "Projecto estruturado",
  estimated_investment: {
    amount: 2500000,
    currency: "USD",
  },
  financing_type: "Capital e parceria técnica",
  impact: "Emprego local e substituição de importações.",
  is_featured: true,
  updated_at: "2026-08-20T10:30:00Z",
};
describe(
"OpportunityCard", () => {
  
it(
"renderiza o título da oportunidade", () => {
    
render(
<OpportunityCard opportunity={opportunity} />);
    
expect(
      
screen.getByText
("Unidade de processamento agrícola em Nampula")
    
).toBeInTheDocument
();
  });
  
it(
"renderiza etiquetas principais", () => {
    
render(
<OpportunityCard opportunity={opportunity} />);
    expect(
screen.getByText
("Destaque")
).toBeInTheDocument
();
    expect(
screen.getByText
("Agronegócio")
).toBeInTheDocument
();
    
expect(screen.getByText
("Projecto estruturado")
).toBeInTheDocument
();
  });
  
it(
"renderiza botões com links correctos", () => {
    
render(
<OpportunityCard opportunity={opportunity} />);
    
expect(screen.getByRole
("link", 
{ name
: /ver ficha/
i })).toHaveAttribute
(
      "href",
      "/oportunidades/unidade-processamento-agricola-nampula"
    );
    
expect(
      
screen.getByRole
("link", 
{ name
: /manifestar interesse/
i }
)
    
).toHaveAttribute
(
      "href",
      "/manifestar-interesse?opportunity=unidade-processamento-agricola-nampula"
    );
  });
});
FeaturedOpportunities.test.jsx
Caminho:
src/components/opportunities/__tests__/FeaturedOpportunities.test.jsx
import 
{ render
, 
screen }
 from "@testing-library/react";
import FeaturedOpportunities from 
"..
/FeaturedOpportunities";
const opportunities = [
  {
    id: 1,
    reference: "INV-MZ-AGR-001",
    title: "Unidade agrícola em Nampula",
    slug: "unidade-agricola-nampula",
    summary: "Projecto de processamento agrícola.",
    sector: "Agronegócio",
    province: "Nampula",
    maturity: "Projecto estruturado",
  },
  {
    id: 2,
    reference: "INV-MZ-ENE-002",
    title: "Sistema solar para agroindústria",
    slug: "sistema-solar-agroindustria",
    summary: "Energia renovável aplicada a unidades produtivas.",
    sector: "Energia",
    province: "Manica",
    maturity: "Pré-estudo disponível",
  },
];
describe(
"FeaturedOpportunities", () => {
  
it(
"renderiza o título da secção", () => {
    
render(
<FeaturedOpportunities opportunities={opportunities} />);
    
expect(
      
screen.getByRole
("heading", 
{ name
: /oportunidades em destaque/
i }
)
    
).toBeInTheDocument
();
  });
  
it(
"renderiza lista de oportunidades", () => {
    
render(
<FeaturedOpportunities opportunities={opportunities} />);
    
expect(screen.getByText
("Unidade agrícola em Nampula")
).toBeInTheDocument
();
    
expect(screen.getByText
("Sistema solar para agroindústria")
).toBeInTheDocument
();
  });
  
it(
"mostra estado vazio quando não há oportunidades", () => {
    
render(
<FeaturedOpportunities opportunities
={
[]} />);
    
expect(screen.getByText
("Sem oportunidades em destaque")
).toBeInTheDocument
();
    
expect(
      
screen.getByText
("Ainda não existem oportunidades em destaque publicadas.")
    
).toBeInTheDocument
();
  });
  
it(
"tem link acessível para ver todas", () => {
    
render(
<FeaturedOpportunities opportunities={opportunities} />);
    const link = 
screen.getByRole
("link", {
      name: /ver todas as oportunidades de investimento/i,
    });
    expect(link
).toHaveAttribute
("href", "/oportunidades");
  });
});
Scripts no 
package.json
{
  "scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:coverage": "vitest --coverage"
  }
}
Próximo passo recomendado
Eu recomendo criar agora:
LatestOpportunities.test.jsx
, reutilizando a mesma lógica do FeaturedOpportunities;
testes para homeService.js
, simulando respostas da API;
um pequeno conjunto de dados mockados
 em src/mocks/opportunities.js para facilitar desenvolvimento visual sem depender sempre do Laravel.
1. LatestOpportunities.jsx
Caminho recomendado:
src/components/opportunities/LatestOpportunities.jsx
import OpportunityCard from 
"./
OpportunityCard";
export default function 
LatestOpportunities(
{
  title = "Oportunidades mais recentes",
  description = "Projectos recentemente publicados ou actualizados na plataforma.",
  opportunities = [],
  viewAllUrl = "/oportunidades?sort=latest",
  limit = 6,
}) {
  const visibleOpportunities = Array.isArray(opportunities)
    ? 
opportunities.slice
(0, limit)
    : [];
  const hasOpportunities = visibleOpportunities.length > 0;
  return (
    <section
      aria-labelledby="latest-opportunities-title"
      className="bg-white py-12 sm:py-16"
    >
      <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <p className="mb-2 inline-flex rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700">
              Actualizações recentes
            </p>
            <h2
              id="latest-opportunities-title"
              className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl"
            >
              {title}
            </h2>
            {description && (
              <p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600 sm:text-base">
                {description}
              </p>
            )}
          </div>
          {viewAllUrl && (
            <a
              href={viewAllUrl}
              className="inline-flex items-center justify-center rounded-xl border border-emerald-200 px-4 py-2 text-sm font-semibold text-emerald-700 transition hover:bg-emerald-50 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
              aria-label="Ver todas as oportunidades mais recentes"
            >
              Ver recentes →
            </a>
          )}
        </div>
        
{!hasOpportunities
 ? (
          <div
            role="status"
            aria-live="polite"
            className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 p-8 text-center"
          >
            <h3 className="text-lg font-semibold text-slate-900">
              Ainda não existem oportunidades recentes
            </h3>
            <p className="mt-2 text-sm text-slate-600">
              Assim que novas oportunidades forem publicadas ou actualizadas,
              elas serão apresentadas nesta área.
            </p>
          </div>
        ) : (
          <ul
            className="grid list-none gap-6 p-0 md:grid-cols-2 lg:grid-cols-3"
            aria-label="Lista de oportunidades mais recentes"
          >
            {visibleOpportunities.map((opportunity) => (
              <li key
={
opportunity.id || 
opportunity.reference
 || 
opportunity.slug
}>
                <OpportunityCard opportunity={opportunity} />
              </li>
            ))}
          </ul>
        )}
      </div>
    </section>
  );
}
2. Testes para homeService.js
Instalação necessária
npm install -D vitest jsdom
Se ainda não tiver React Testing Library configurado:
npm install -D @testing-library/react @testing-library/jest-dom
No vite.config.js:
import 
{ defineConfig
 } from "vite";
import react from "@vitejs/plugin-react";
export default 
defineConfig(
{
  plugins: [
react(
)],
  test: {
    environment: "jsdom",
    setupFiles: "./src/test/setup.js",
  },
});