1305 lines
61 KiB
TypeScript
1305 lines
61 KiB
TypeScript
import { PublicSectorCards } from '@/components/public-sector-cards';
|
|
import { PORTAL_MORE_LINK } from '@/lib/portal-presentation';
|
|
import {
|
|
PublicDocumentContent,
|
|
type PublicDocument,
|
|
} from '@/components/public-document-content';
|
|
import { Head, Link, useForm, usePage } from '@inertiajs/react';
|
|
import { useState } from 'react';
|
|
import { PortalMenu } from '@/layouts/portal-navigation';
|
|
import { newSubmissionKey } from '@/lib/portal-api';
|
|
import { ProvinceSelect } from '@/components/investment-territory';
|
|
import { SimulationForm } from '@/components/simulation-form';
|
|
import { InvestmentRoadmap } from '@/components/investment-roadmap';
|
|
import { JourneyDocumentResources } from '@/components/journey-document-resources';
|
|
import SectorEntry, { type SectorEntryData } from '@/components/sector-entry';
|
|
import CountrySelect, { type Country } from '@/components/country-select';
|
|
|
|
const input =
|
|
'box-border w-full min-w-0 max-w-full rounded-lg border border-slate-300 bg-white p-3 text-slate-900';
|
|
const button =
|
|
'inline-block rounded-lg bg-emerald-800 px-5 py-3 font-semibold text-white disabled:opacity-50';
|
|
const titles: Record<string, string> = {
|
|
sectors: 'Sectores de investimento',
|
|
sector: 'Sector prioritário',
|
|
procedures: 'Como Investir em Moçambique',
|
|
legislation: 'Legislação e Incentivos',
|
|
documents: 'Documentos e Formulários',
|
|
support: 'Apoio ao Investidor',
|
|
partnerships: 'Bolsa de Parcerias',
|
|
simulator: 'Simulador Fiscal',
|
|
investor: 'Formulário de investidor',
|
|
company: 'Formulário de empresa',
|
|
desk: 'Balcão Virtual',
|
|
};
|
|
const documentTypeLabels: Record<string, string> = {
|
|
legislation: 'Legislação',
|
|
procedure: 'Procedimento oficial',
|
|
official_form: 'Formulário oficial',
|
|
digital_adaptation: 'Adaptação digital de formulário',
|
|
guidance_template: 'Modelo orientativo',
|
|
guide: 'Guia',
|
|
study: 'Estudo',
|
|
reference_data: 'Dados de referência',
|
|
unknown: 'Tipo por classificar',
|
|
};
|
|
const legalStatusLabels: Record<string, string> = {
|
|
not_applicable: 'Não aplicável',
|
|
in_force: 'Vigência indicada; confirme o período',
|
|
historical: 'Histórico',
|
|
proposed: 'Proposta; não é norma vigente',
|
|
repealed: 'Revogada',
|
|
amended: 'Alterada; consulte alterações posteriores',
|
|
unknown: 'Por confirmar',
|
|
};
|
|
type Resource = PublicDocument & { document_type: string };
|
|
type Partnership = {
|
|
id: number;
|
|
title: string;
|
|
sector: string;
|
|
province: string;
|
|
offer: string;
|
|
seeking: string;
|
|
};
|
|
type Page<T> = {
|
|
data: T[];
|
|
prev_page_url: string | null;
|
|
next_page_url: string | null;
|
|
total: number;
|
|
};
|
|
type Props = {
|
|
sectorEntry?: SectorEntryData;
|
|
countries?: Country[];
|
|
section: string;
|
|
filters?: Record<string, string | string[]>;
|
|
sectors?: {
|
|
name: string;
|
|
slug: string;
|
|
short_description: string | null;
|
|
opportunities_count: number;
|
|
url?: string;
|
|
specializations?: { id: number; name: string; url: string }[];
|
|
}[];
|
|
sector?: {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
short_description: string;
|
|
};
|
|
opportunities?: {
|
|
title: string;
|
|
slug: string;
|
|
province: string;
|
|
summary: string;
|
|
}[];
|
|
resources?: Page<Resource>;
|
|
categories?: string[];
|
|
topics?: string[];
|
|
documentTypes?: string[];
|
|
languages?: string[];
|
|
issuers?: string[];
|
|
legalStatuses?: string[];
|
|
territories?: {
|
|
id: string;
|
|
name: string;
|
|
location_type: string;
|
|
parent_id: string | null;
|
|
}[];
|
|
activities?: string[];
|
|
stages?: string[];
|
|
modules?: string[];
|
|
partnerships?: Page<Partnership>;
|
|
contacts?: {
|
|
email?: string;
|
|
phone?: string;
|
|
address?: string;
|
|
hours?: string;
|
|
};
|
|
requests?: {
|
|
reference: string;
|
|
status: string;
|
|
created_at: string;
|
|
metadata: { kind: string };
|
|
}[];
|
|
myPartnerships?: { id: number; title: string; status: string }[];
|
|
};
|
|
const states: Record<string, string> = {
|
|
received: 'Recebido',
|
|
in_review: 'Em análise',
|
|
contacted: 'Contactado',
|
|
closed: 'Encerrado',
|
|
pending: 'A aguardar publicação',
|
|
published: 'Publicado',
|
|
withdrawn: 'Retirado',
|
|
};
|
|
function Pagination({ page }: { page: Page<unknown> }) {
|
|
return (
|
|
<nav aria-label="Paginação" className="flex gap-6 py-5">
|
|
{page.prev_page_url && (
|
|
<Link href={page.prev_page_url}>Anterior</Link>
|
|
)}
|
|
<span>{page.total} registos</span>
|
|
{page.next_page_url && (
|
|
<Link href={page.next_page_url}>Seguinte</Link>
|
|
)}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
function RequestForm({
|
|
kind,
|
|
partnershipId,
|
|
}: {
|
|
kind: string;
|
|
partnershipId?: number;
|
|
}) {
|
|
const { auth } = usePage<{
|
|
auth: { user: { name: string; email: string } | null };
|
|
}>().props;
|
|
const countries =
|
|
usePage<{ countries?: Country[] }>().props.countries ?? [];
|
|
const form = useForm({
|
|
kind,
|
|
fullName: auth.user?.name ?? '',
|
|
email: auth.user?.email ?? '',
|
|
phone: '',
|
|
country: '',
|
|
declared_origin_country_code: '',
|
|
company: '',
|
|
sectorInterest: '',
|
|
message: '',
|
|
privacyConsent: false,
|
|
submission_key: '',
|
|
partnership_id: partnershipId ?? null,
|
|
});
|
|
const [sent, setSent] = useState(false);
|
|
return (
|
|
<form
|
|
className="space-y-4 rounded-xl border bg-white p-6"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const key = form.data.submission_key || newSubmissionKey();
|
|
form.setData('submission_key', key);
|
|
form.transform((data) => ({ ...data, submission_key: key }));
|
|
form.post('/apoio', {
|
|
preserveScroll: true,
|
|
onSuccess: () => setSent(true),
|
|
});
|
|
}}
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
{kind === 'company'
|
|
? 'Dados da empresa e do representante'
|
|
: 'Enviar pedido'}
|
|
</h2>
|
|
{!auth.user && (
|
|
<p className="text-sm">
|
|
Pode enviar sem conta. Para acompanhar no Balcão Virtual,{' '}
|
|
<Link href="/login" className="underline">
|
|
entre antes de enviar
|
|
</Link>
|
|
.
|
|
</p>
|
|
)}
|
|
{(
|
|
[
|
|
'fullName',
|
|
'email',
|
|
'phone',
|
|
'country',
|
|
'sectorInterest',
|
|
...(kind === 'company' ? ['company'] : []),
|
|
] as const
|
|
).map((key) => {
|
|
const field = key as
|
|
| 'fullName'
|
|
| 'email'
|
|
| 'phone'
|
|
| 'country'
|
|
| 'sectorInterest'
|
|
| 'company';
|
|
const labels = {
|
|
fullName: 'Nome do representante',
|
|
email: 'Email',
|
|
phone: 'Telefone (opcional)',
|
|
country: 'País',
|
|
sectorInterest: 'Sector ou assunto',
|
|
company: 'Nome da empresa',
|
|
};
|
|
return (
|
|
<label key={field} className="block space-y-1">
|
|
<span>{labels[field]}</span>
|
|
{field === 'country' ? (
|
|
<CountrySelect
|
|
countries={countries}
|
|
label="País do requerente"
|
|
value={form.data.country}
|
|
onChange={(value) =>
|
|
form.setData('country', value)
|
|
}
|
|
required
|
|
allowLegacyValue
|
|
/>
|
|
) : (
|
|
<input
|
|
className={input}
|
|
type={field === 'email' ? 'email' : 'text'}
|
|
required={field !== 'phone'}
|
|
maxLength={field === 'phone' ? 30 : 150}
|
|
value={form.data[field]}
|
|
onChange={(e) =>
|
|
form.setData(field, e.target.value)
|
|
}
|
|
/>
|
|
)}
|
|
</label>
|
|
);
|
|
})}
|
|
<label className="block">
|
|
País de origem declarado desta solicitação
|
|
<CountrySelect
|
|
countries={countries}
|
|
label="País de origem declarado desta solicitação"
|
|
value={form.data.declared_origin_country_code}
|
|
onChange={(value) =>
|
|
form.setData('declared_origin_country_code', value)
|
|
}
|
|
/>
|
|
</label>
|
|
<p className="text-sm">
|
|
Origem opcional e independente do país acima. Sem declaração,
|
|
permanece não informado nas estatísticas.
|
|
</p>
|
|
<label className="block">
|
|
Descreva o pedido
|
|
<textarea
|
|
className={input}
|
|
required
|
|
minLength={20}
|
|
maxLength={2000}
|
|
rows={5}
|
|
value={form.data.message}
|
|
onChange={(e) => form.setData('message', e.target.value)}
|
|
/>
|
|
</label>
|
|
<label className="flex items-start gap-3">
|
|
<input
|
|
type="checkbox"
|
|
required
|
|
checked={form.data.privacyConsent}
|
|
onChange={(e) =>
|
|
form.setData('privacyConsent', e.target.checked)
|
|
}
|
|
/>
|
|
<span>
|
|
Autorizo o tratamento destes dados para responder e
|
|
acompanhar este pedido. Os dados ficam acessíveis à equipa
|
|
de atendimento; não serão publicados na bolsa.
|
|
</span>
|
|
</label>
|
|
{Object.entries(form.errors).map(([key, error]) => (
|
|
<p key={key} role="alert" className="text-red-700">
|
|
{error}
|
|
</p>
|
|
))}
|
|
{sent ? (
|
|
<p role="status">
|
|
Pedido enviado. Guarde o protocolo apresentado acima.{' '}
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() => {
|
|
setSent(false);
|
|
form.setData('submission_key', '');
|
|
form.setData('message', '');
|
|
}}
|
|
>
|
|
Enviar novo pedido
|
|
</button>
|
|
</p>
|
|
) : (
|
|
<button className={button} disabled={form.processing}>
|
|
Enviar e obter protocolo
|
|
</button>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function PartnershipForm() {
|
|
const form = useForm({
|
|
title: '',
|
|
sector: '',
|
|
province: '',
|
|
offer: '',
|
|
seeking: '',
|
|
publication_consent: false,
|
|
});
|
|
return (
|
|
<form
|
|
className="space-y-4 rounded-xl border p-6"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.post('/parcerias', {
|
|
preserveScroll: true,
|
|
onSuccess: () => form.reset(),
|
|
});
|
|
}}
|
|
>
|
|
<h2 className="text-xl font-bold">Propor parceria</h2>
|
|
<p>
|
|
Descreva apenas informação destinada a publicação. Não inclua
|
|
contactos pessoais ou documentos confidenciais.
|
|
</p>
|
|
{(['title', 'sector', 'province', 'offer', 'seeking'] as const).map(
|
|
(key) => (
|
|
<label className="block" key={key}>
|
|
{
|
|
{
|
|
title: 'Título',
|
|
sector: 'Sector',
|
|
province: 'Província',
|
|
offer: 'O que oferece',
|
|
seeking: 'Parceiro ou capacidade que procura',
|
|
}[key]
|
|
}
|
|
{key === 'province' ? (
|
|
<ProvinceSelect
|
|
value={form.data.province}
|
|
onChange={(province) =>
|
|
form.setData('province', province)
|
|
}
|
|
className={input}
|
|
required
|
|
placeholder="Seleccione uma província"
|
|
label="Província"
|
|
/>
|
|
) : (
|
|
<textarea
|
|
className={input}
|
|
required
|
|
minLength={
|
|
key === 'offer' || key === 'seeking'
|
|
? 20
|
|
: 1
|
|
}
|
|
maxLength={
|
|
key === 'offer' || key === 'seeking'
|
|
? 2000
|
|
: 100
|
|
}
|
|
value={form.data[key]}
|
|
onChange={(e) =>
|
|
form.setData(key, e.target.value)
|
|
}
|
|
/>
|
|
)}
|
|
</label>
|
|
),
|
|
)}
|
|
<label className="flex gap-2">
|
|
<input
|
|
type="checkbox"
|
|
required
|
|
checked={form.data.publication_consent}
|
|
onChange={(e) =>
|
|
form.setData('publication_consent', e.target.checked)
|
|
}
|
|
/>
|
|
Autorizo a publicação dos dados desta proposta após revisão.
|
|
</label>
|
|
{Object.entries(form.errors).map(([key, error]) => (
|
|
<p role="alert" key={key}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
<button disabled={form.processing} className={button}>
|
|
Submeter proposta
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function Simulator() {
|
|
const page = usePage<{ auth: { user: { id: number } | null } }>();
|
|
const query = new URL(page.url, 'https://investmoz.invalid').searchParams;
|
|
const scope = `${page.props.auth.user?.id ?? 'anonymous'}:${query.get('context') ?? ''}:${query.get('draft') ?? ''}`;
|
|
return (
|
|
<div className="space-y-5">
|
|
<p>
|
|
<Link href="/como-investir" className="underline">
|
|
Voltar ao roteiro de investimento
|
|
</Link>{' '}
|
|
para rever mercado, localização, recursos, formalização e
|
|
execução.
|
|
</p>
|
|
<SimulationForm key={scope} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Services(p: Props) {
|
|
const { flash, auth } = usePage<{
|
|
flash?: { success?: string };
|
|
auth: { user: unknown };
|
|
}>().props;
|
|
const currentUrl = usePage<{ url: string }>().url;
|
|
const filter = useForm({
|
|
q: typeof p.filters?.q === 'string' ? p.filters.q : '',
|
|
category:
|
|
typeof p.filters?.category === 'string' ? p.filters.category : '',
|
|
topic: typeof p.filters?.topic === 'string' ? p.filters.topic : '',
|
|
sector: typeof p.filters?.sector === 'string' ? p.filters.sector : '',
|
|
province:
|
|
typeof p.filters?.province === 'string' ? p.filters.province : '',
|
|
document_type:
|
|
typeof p.filters?.document_type === 'string'
|
|
? p.filters.document_type
|
|
: '',
|
|
language:
|
|
typeof p.filters?.language === 'string' ? p.filters.language : '',
|
|
issuer: typeof p.filters?.issuer === 'string' ? p.filters.issuer : '',
|
|
legal_status:
|
|
typeof p.filters?.legal_status === 'string'
|
|
? p.filters.legal_status
|
|
: '',
|
|
source_confirmation:
|
|
typeof p.filters?.source_confirmation === 'string'
|
|
? p.filters.source_confirmation
|
|
: '',
|
|
territory_id:
|
|
typeof p.filters?.territory_id === 'string'
|
|
? p.filters.territory_id
|
|
: '',
|
|
activity: Array.isArray(p.filters?.activity) ? p.filters.activity : [],
|
|
stage: Array.isArray(p.filters?.stage) ? p.filters.stage : [],
|
|
module: Array.isArray(p.filters?.module) ? p.filters.module : [],
|
|
});
|
|
const [selectedPartner, setSelectedPartner] = useState<number>();
|
|
const title = p.sector?.name ?? titles[p.section];
|
|
const formTypes = [
|
|
'official_form',
|
|
'digital_adaptation',
|
|
'guidance_template',
|
|
];
|
|
const firstFormIndex =
|
|
p.resources?.data.findIndex((resource) =>
|
|
formTypes.includes(resource.document_type),
|
|
) ?? -1;
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 text-slate-900">
|
|
<Head title={title} />
|
|
<PortalMenu />
|
|
<main className="mx-auto max-w-6xl min-w-0 space-y-8 overflow-x-clip px-5 py-10">
|
|
<h1
|
|
className={
|
|
p.section === 'sectors'
|
|
? 'text-3xl font-bold text-[#ed1c24]'
|
|
: 'text-3xl font-bold'
|
|
}
|
|
>
|
|
{title}
|
|
</h1>
|
|
{flash?.success && (
|
|
<p
|
|
role="status"
|
|
className="rounded border border-emerald-700 bg-emerald-50 p-4"
|
|
>
|
|
{flash.success}
|
|
</p>
|
|
)}
|
|
{p.section === 'sectors' && (
|
|
<section
|
|
aria-label="Todos os sectores de investimento"
|
|
className="space-y-6"
|
|
>
|
|
<p className="text-slate-600">
|
|
Explore o potencial estratégico de cada sector em
|
|
Moçambique.
|
|
</p>
|
|
<PublicSectorCards
|
|
sectors={(p.sectors ?? []).map((sector) => ({
|
|
slug: sector.slug,
|
|
name: sector.name,
|
|
description: sector.short_description,
|
|
opportunitiesCount: sector.opportunities_count,
|
|
specializations: sector.specializations,
|
|
url: sector.url,
|
|
}))}
|
|
/>
|
|
{!p.sectors?.length && (
|
|
<p>Não existem sectores publicados.</p>
|
|
)}
|
|
<div className="flex justify-center">
|
|
<a href="/" className={PORTAL_MORE_LINK}>
|
|
Voltar ao início
|
|
</a>
|
|
</div>
|
|
</section>
|
|
)}
|
|
{p.section === 'sector' && p.sector && (
|
|
<>
|
|
<p className="whitespace-pre-line">
|
|
{p.sector.description ||
|
|
p.sector.short_description ||
|
|
'Descrição sectorial em preparação.'}
|
|
</p>
|
|
{!p.sectorEntry && (
|
|
<Link
|
|
className={button}
|
|
href={
|
|
'/investimentos/iniciar?source=sector:' +
|
|
p.sector.id
|
|
}
|
|
>
|
|
Iniciar ou continuar uma ideia neste sector
|
|
</Link>
|
|
)}
|
|
{p.sectorEntry && (
|
|
<SectorEntry
|
|
data={{
|
|
...p.sectorEntry,
|
|
start_href: p.sectorEntry.start_href
|
|
? '/investimentos/iniciar?source=sector:' +
|
|
p.sector.id
|
|
: null,
|
|
}}
|
|
/>
|
|
)}
|
|
<h2 className="text-xl font-bold">
|
|
Oportunidades no sector
|
|
</h2>
|
|
{p.opportunities?.map((o) => (
|
|
<article
|
|
key={o.slug}
|
|
className="rounded border bg-white p-5"
|
|
>
|
|
<Link
|
|
className="text-lg font-bold underline"
|
|
href={`/oportunidades/${o.slug}`}
|
|
>
|
|
{o.title}
|
|
</Link>
|
|
<p>{o.province}</p>
|
|
<p>{o.summary}</p>
|
|
<ul aria-label="Localizações associadas">
|
|
{p.sectorEntry?.locations
|
|
.filter(
|
|
(l) =>
|
|
l.href ===
|
|
`/oportunidades/${o.slug}`,
|
|
)
|
|
.filter(
|
|
(l, i, rows) =>
|
|
rows.findIndex(
|
|
(other) =>
|
|
JSON.stringify(
|
|
other.location,
|
|
) ===
|
|
JSON.stringify(
|
|
l.location,
|
|
),
|
|
) === i,
|
|
)
|
|
.map((l, i) => (
|
|
<li key={i}>
|
|
{typeof l.location.name ===
|
|
'string'
|
|
? l.location.name
|
|
: 'Consultar localização e fonte na oportunidade'}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</article>
|
|
))}
|
|
{!p.opportunities?.length && (
|
|
<p>Sem oportunidades publicadas neste sector.</p>
|
|
)}
|
|
<Link
|
|
className={button}
|
|
href={`/oportunidades?sector=${p.sector.slug}`}
|
|
>
|
|
Pesquisar todas as oportunidades do sector
|
|
</Link>
|
|
<p>
|
|
<Link href="/como-investir" className="underline">
|
|
Preparar o investimento
|
|
</Link>{' '}
|
|
·{' '}
|
|
<Link href="/apoio" className="underline">
|
|
Pedir apoio sectorial
|
|
</Link>
|
|
</p>
|
|
</>
|
|
)}
|
|
{['procedures', 'documents', 'legislation'].includes(
|
|
p.section,
|
|
) && <JourneyDocumentResources stages={p.stages} />}
|
|
{p.section === 'procedures' && <InvestmentRoadmap />}
|
|
{(p.section === 'documents' || p.section === 'legislation') && (
|
|
<>
|
|
<form
|
|
className="grid min-w-0 gap-3 sm:grid-cols-2 lg:grid-cols-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
filter.get(
|
|
p.section === 'documents'
|
|
? '/documentos'
|
|
: '/legislacao',
|
|
);
|
|
}}
|
|
>
|
|
<label className="block min-w-0">
|
|
Pesquisar
|
|
<input
|
|
className={input}
|
|
value={filter.data.q}
|
|
onChange={(e) =>
|
|
filter.setData('q', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
{!!(p.topics?.length || filter.data.topic) && (
|
|
<label className="block min-w-0">
|
|
Tema
|
|
<select
|
|
className={input}
|
|
value={filter.data.topic}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'topic',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todos</option>
|
|
{p.topics?.map((topic) => (
|
|
<option key={topic}>{topic}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{!!(
|
|
p.categories?.length || filter.data.category
|
|
) && (
|
|
<label className="block min-w-0">
|
|
Categoria
|
|
<select
|
|
className={input}
|
|
value={filter.data.category}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'category',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todas</option>
|
|
{p.categories?.map((category) => (
|
|
<option
|
|
key={category}
|
|
value={category}
|
|
>
|
|
{documentTypeLabels[category] ??
|
|
category}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{!!(
|
|
p.documentTypes?.length ||
|
|
filter.data.document_type
|
|
) && (
|
|
<label className="block min-w-0">
|
|
Tipo documental
|
|
<select
|
|
className={input}
|
|
value={filter.data.document_type}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'document_type',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todos</option>
|
|
{p.documentTypes?.map((type) => (
|
|
<option key={type} value={type}>
|
|
{documentTypeLabels[type] ??
|
|
'Documento'}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{!!p.activities?.length && (
|
|
<fieldset className="min-w-0 rounded border p-3">
|
|
<legend className="px-1">
|
|
Actividade ou CAE
|
|
</legend>
|
|
<div className="max-h-36 space-y-2 overflow-y-auto">
|
|
{p.activities.map((activity) => (
|
|
<label
|
|
key={activity}
|
|
className="flex items-start gap-2 break-words"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={filter.data.activity.includes(
|
|
activity,
|
|
)}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'activity',
|
|
e.target.checked
|
|
? [
|
|
...filter
|
|
.data
|
|
.activity,
|
|
activity,
|
|
]
|
|
: filter.data.activity.filter(
|
|
(value) =>
|
|
value !==
|
|
activity,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
<span>{activity}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
)}
|
|
{!!p.stages?.length && (
|
|
<fieldset className="min-w-0 rounded border p-3">
|
|
<legend className="px-1">
|
|
Etapa do projecto
|
|
</legend>
|
|
<div className="max-h-36 space-y-2 overflow-y-auto">
|
|
{p.stages.map((stage) => (
|
|
<label
|
|
key={stage}
|
|
className="flex items-start gap-2 break-words"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={filter.data.stage.includes(
|
|
stage,
|
|
)}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'stage',
|
|
e.target.checked
|
|
? [
|
|
...filter
|
|
.data
|
|
.stage,
|
|
stage,
|
|
]
|
|
: filter.data.stage.filter(
|
|
(value) =>
|
|
value !==
|
|
stage,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
<span>{stage}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
)}
|
|
{!!p.modules?.length && (
|
|
<fieldset className="min-w-0 rounded border p-3">
|
|
<legend className="px-1">Módulo</legend>
|
|
<div className="max-h-36 space-y-2 overflow-y-auto">
|
|
{p.modules.map((module) => (
|
|
<label
|
|
key={module}
|
|
className="flex items-start gap-2 break-words"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={filter.data.module.includes(
|
|
module,
|
|
)}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'module',
|
|
e.target.checked
|
|
? [
|
|
...filter
|
|
.data
|
|
.module,
|
|
module,
|
|
]
|
|
: filter.data.module.filter(
|
|
(value) =>
|
|
value !==
|
|
module,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
<span>
|
|
{{
|
|
simulator:
|
|
'Simulador fiscal',
|
|
procedures:
|
|
'Como investir',
|
|
investment:
|
|
'Oportunidades',
|
|
zones: 'Zonas e parques',
|
|
education: 'Educação',
|
|
business: 'Negócios',
|
|
land: 'Terra e DUAT',
|
|
}[module] ??
|
|
'Documentos de apoio'}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
)}
|
|
{!!(p.issuers?.length || filter.data.issuer) && (
|
|
<label className="block min-w-0">
|
|
Entidade emitente
|
|
<select
|
|
className={input}
|
|
value={filter.data.issuer}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'issuer',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todas</option>
|
|
{p.issuers?.map((issuer) => (
|
|
<option key={issuer}>
|
|
{issuer}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{!!(
|
|
p.languages?.length || filter.data.language
|
|
) && (
|
|
<label className="block min-w-0">
|
|
Idioma
|
|
<select
|
|
className={input}
|
|
value={filter.data.language}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'language',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todos</option>
|
|
{p.languages?.map((language) => (
|
|
<option key={language}>
|
|
{language}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{!!(
|
|
p.legalStatuses?.length ||
|
|
filter.data.legal_status
|
|
) && (
|
|
<label className="block min-w-0">
|
|
Situação jurídica indicada
|
|
<select
|
|
className={input}
|
|
value={filter.data.legal_status}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'legal_status',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todas</option>
|
|
{p.legalStatuses?.map((status) => (
|
|
<option key={status} value={status}>
|
|
{legalStatusLabels[status] ??
|
|
'Por confirmar'}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
<label className="block min-w-0">
|
|
Confirmação da fonte
|
|
<select
|
|
className={input}
|
|
value={filter.data.source_confirmation}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'source_confirmation',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todas</option>
|
|
<option value="official_confirmed">
|
|
Fonte oficial confirmada
|
|
</option>
|
|
<option value="identified">
|
|
Origem identificada
|
|
</option>
|
|
<option value="unconfirmed">
|
|
Por confirmar
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Âmbito territorial
|
|
<select
|
|
className={input}
|
|
value={filter.data.territory_id}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
'territory_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Todos</option>
|
|
{p.territories?.map((territory) => (
|
|
<option
|
|
key={territory.id}
|
|
value={territory.id}
|
|
>
|
|
{territory.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="text-xs text-slate-600">
|
|
Documentos nacionais aplicáveis permanecem
|
|
incluídos.
|
|
</span>
|
|
</label>
|
|
<button className={button}>Pesquisar</button>
|
|
<Link
|
|
className="self-center underline"
|
|
href={
|
|
p.section === 'documents'
|
|
? '/documentos'
|
|
: '/legislacao'
|
|
}
|
|
>
|
|
Limpar filtros
|
|
</Link>
|
|
</form>
|
|
{!!p.topics?.length && (
|
|
<nav
|
|
aria-label="Navegar por assunto"
|
|
className="flex flex-wrap gap-2"
|
|
>
|
|
<span className="font-semibold">Assuntos:</span>
|
|
{p.topics.map((topic) => (
|
|
<Link
|
|
key={topic}
|
|
className="rounded-full border bg-white px-3 py-1 underline"
|
|
href={`${p.section === 'documents' ? '/documentos' : '/legislacao'}?${new URLSearchParams({ topic }).toString()}`}
|
|
>
|
|
{topic}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
)}
|
|
<p className="text-sm">
|
|
Os documentos nacionais aplicáveis continuam
|
|
incluídos numa pesquisa territorial e são
|
|
identificados como nacionais. A localização da
|
|
entidade emitente não define o âmbito de aplicação.
|
|
</p>
|
|
{p.resources?.data.map((r, index) => (
|
|
<div key={r.id} className="contents">
|
|
{index === 0 && (
|
|
<h2 className="pt-4 text-xl font-bold">
|
|
{firstFormIndex === 0
|
|
? 'Formulários e modelos'
|
|
: 'Documentos de consulta'}
|
|
</h2>
|
|
)}
|
|
{index === firstFormIndex &&
|
|
firstFormIndex > 0 && (
|
|
<h2 className="pt-4 text-xl font-bold">
|
|
Formulários e modelos
|
|
</h2>
|
|
)}
|
|
<article
|
|
key={r.id}
|
|
className="min-w-0 space-y-2 overflow-hidden rounded-xl border bg-white p-5"
|
|
>
|
|
<PublicDocumentContent
|
|
resource={r}
|
|
back={currentUrl}
|
|
/>
|
|
</article>
|
|
</div>
|
|
))}
|
|
{!p.resources?.data.length && (
|
|
<p>
|
|
Não existem documentos publicados para esta
|
|
pesquisa.{' '}
|
|
<Link href="/apoio" className="underline">
|
|
Solicitar documento ao apoio
|
|
</Link>
|
|
.
|
|
</p>
|
|
)}
|
|
{p.resources && <Pagination page={p.resources} />}
|
|
<div className="flex flex-wrap gap-4">
|
|
<Link
|
|
className={button}
|
|
href="/formularios/investidor"
|
|
>
|
|
Formulário de investidor
|
|
</Link>
|
|
<Link
|
|
className={button}
|
|
href="/formularios/empresa"
|
|
>
|
|
Formulário de empresa
|
|
</Link>
|
|
</div>
|
|
</>
|
|
)}
|
|
{['support', 'investor', 'company'].includes(p.section) && (
|
|
<RequestForm
|
|
key={p.section}
|
|
kind={p.section === 'support' ? 'support' : p.section}
|
|
/>
|
|
)}
|
|
{p.section === 'support' && (
|
|
<>
|
|
<section className="space-y-3">
|
|
<h2 className="text-xl font-bold">Contactos</h2>
|
|
{p.contacts?.email && (
|
|
<p>
|
|
Email:{' '}
|
|
<a
|
|
className="underline"
|
|
href={`mailto:${p.contacts.email}`}
|
|
>
|
|
{p.contacts.email}
|
|
</a>
|
|
</p>
|
|
)}
|
|
{p.contacts?.phone && (
|
|
<p>Telefone: {p.contacts.phone}</p>
|
|
)}
|
|
{p.contacts?.address && <p>{p.contacts.address}</p>}
|
|
{p.contacts?.hours && (
|
|
<p>Horário: {p.contacts.hours}</p>
|
|
)}
|
|
{!p.contacts?.email && !p.contacts?.phone && (
|
|
<p>
|
|
Utilize o formulário para contactar a
|
|
equipa. Os contactos directos ainda não
|
|
estão publicados.
|
|
</p>
|
|
)}
|
|
</section>
|
|
<section className="space-y-3">
|
|
<h2 className="text-xl font-bold">
|
|
Perguntas frequentes
|
|
</h2>
|
|
{[
|
|
[
|
|
'Como acompanho um pedido?',
|
|
'Entre antes de enviar o pedido. O protocolo e o estado ficam disponíveis no Balcão Virtual. Para pedidos enviados sem conta, conserve o protocolo e indique-o nos contactos seguintes.',
|
|
],
|
|
[
|
|
'Manifestar interesse garante financiamento ou aprovação?',
|
|
'O envio regista o interesse para análise e contacto da equipa. Não representa aprovação do investimento nem compromisso de financiamento.',
|
|
],
|
|
[
|
|
'Como publico uma parceria?',
|
|
'Entre com email verificado, preencha a proposta na Bolsa de Parcerias e autorize a publicação. A gestão revê a proposta antes de a tornar pública.',
|
|
],
|
|
[
|
|
'Onde obtenho documentos e formulários?',
|
|
'Consulte Documentos e Formulários. Se não encontrar o documento, indique o título ou assunto num pedido de apoio.',
|
|
],
|
|
].map(([q, a]) => (
|
|
<details
|
|
key={q}
|
|
className="rounded border bg-white p-4"
|
|
>
|
|
<summary className="cursor-pointer font-semibold">
|
|
{q}
|
|
</summary>
|
|
<p className="mt-3">{a}</p>
|
|
</details>
|
|
))}
|
|
</section>
|
|
</>
|
|
)}
|
|
{p.section === 'simulator' && <Simulator />}
|
|
{p.section === 'partnerships' && (
|
|
<>
|
|
<p>
|
|
Encontre capacidades e parceiros. O contacto é
|
|
intermediado pelo apoio; os dados privados dos
|
|
proponentes não são publicados.
|
|
</p>
|
|
<form
|
|
className="grid gap-3 md:grid-cols-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
filter.get('/parcerias');
|
|
}}
|
|
>
|
|
{(['q', 'sector', 'province'] as const).map(
|
|
(key) => (
|
|
<label key={key}>
|
|
{
|
|
{
|
|
q: 'Pesquisar',
|
|
sector: 'Sector',
|
|
province: 'Província',
|
|
}[key]
|
|
}
|
|
{key === 'province' ? (
|
|
<ProvinceSelect
|
|
value={filter.data.province}
|
|
onChange={(province) =>
|
|
filter.setData(
|
|
'province',
|
|
province,
|
|
)
|
|
}
|
|
className={input}
|
|
placeholder="Todas as províncias"
|
|
label="Filtrar por província"
|
|
/>
|
|
) : (
|
|
<input
|
|
className={input}
|
|
value={filter.data[key]}
|
|
onChange={(e) =>
|
|
filter.setData(
|
|
key,
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
)}
|
|
</label>
|
|
),
|
|
)}
|
|
<button className={button}>Filtrar</button>
|
|
</form>
|
|
{p.partnerships?.data.map((item) => (
|
|
<article
|
|
key={item.id}
|
|
className="space-y-3 rounded-xl border bg-white p-6"
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
{item.title}
|
|
</h2>
|
|
<p>
|
|
{item.sector} · {item.province}
|
|
</p>
|
|
<h3 className="font-bold">Oferece</h3>
|
|
<p className="whitespace-pre-line">
|
|
{item.offer}
|
|
</p>
|
|
<h3 className="font-bold">Procura</h3>
|
|
<p className="whitespace-pre-line">
|
|
{item.seeking}
|
|
</p>
|
|
<button
|
|
className={button}
|
|
onClick={() => setSelectedPartner(item.id)}
|
|
>
|
|
Pedir contacto sobre esta parceria
|
|
</button>
|
|
</article>
|
|
))}
|
|
{!p.partnerships?.data.length && (
|
|
<p>
|
|
Ainda não existem propostas publicadas para esta
|
|
pesquisa.
|
|
</p>
|
|
)}
|
|
{p.partnerships && <Pagination page={p.partnerships} />}
|
|
{selectedPartner && (
|
|
<RequestForm
|
|
key={selectedPartner}
|
|
kind="partnership"
|
|
partnershipId={selectedPartner}
|
|
/>
|
|
)}{' '}
|
|
{auth.user ? (
|
|
<PartnershipForm />
|
|
) : (
|
|
<Link className={button} href="/login">
|
|
Entrar para propor parceria
|
|
</Link>
|
|
)}
|
|
</>
|
|
)}
|
|
{p.section === 'desk' && (
|
|
<>
|
|
<p>
|
|
Acompanhe os pedidos submetidos com esta conta. Os
|
|
contactos da equipa continuam pelos dados fornecidos
|
|
no pedido.
|
|
</p>
|
|
<Link className={button} href="/apoio">
|
|
Novo pedido
|
|
</Link>
|
|
<h2 className="text-xl font-bold">Os meus pedidos</h2>
|
|
{p.requests?.map((r) => (
|
|
<article
|
|
key={r.reference}
|
|
className="rounded border bg-white p-5"
|
|
>
|
|
<strong>{r.reference}</strong>
|
|
<p>{states[r.status] ?? r.status}</p>
|
|
<p>
|
|
{new Date(r.created_at).toLocaleDateString(
|
|
'pt-MZ',
|
|
)}
|
|
</p>
|
|
</article>
|
|
))}
|
|
{!p.requests?.length && (
|
|
<p>
|
|
Ainda não existem pedidos de apoio enviados com
|
|
esta conta.
|
|
</p>
|
|
)}
|
|
<h2 className="text-xl font-bold">
|
|
As minhas propostas de parceria
|
|
</h2>
|
|
{p.myPartnerships?.map((r) => (
|
|
<p key={r.id}>
|
|
{r.title} — {states[r.status] ?? r.status}
|
|
</p>
|
|
))}
|
|
<Link href="/investidor" className="underline">
|
|
Abrir fichas e manifestações de interesse da
|
|
carteira
|
|
</Link>
|
|
</>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|