475 lines
22 KiB
TypeScript
475 lines
22 KiB
TypeScript
import { Link, useForm } from '@inertiajs/react';
|
|
import { useRef } from 'react';
|
|
|
|
type Profile = {
|
|
model: string;
|
|
fields: Record<string, string>;
|
|
operation_countries: string[];
|
|
activities: Record<string, string>;
|
|
activity_basis: string;
|
|
documents: Record<string, string>;
|
|
};
|
|
export type FintechView = {
|
|
record: { sha256: string; saved_at: string; profile: Profile } | null;
|
|
fields: Record<string, string>;
|
|
activities: Record<string, string>;
|
|
countries: { code: string; name: string }[];
|
|
requirements: {
|
|
code: string;
|
|
activity: string;
|
|
state: string;
|
|
basis: string;
|
|
limit: string;
|
|
}[];
|
|
documents: {
|
|
code: string;
|
|
stage: string;
|
|
label: string;
|
|
document_id: string | null;
|
|
applicability: string;
|
|
action: string;
|
|
}[];
|
|
affected_reviews: string[];
|
|
limits: string[];
|
|
guidance: {
|
|
stage: string;
|
|
recommendations: { code: string; explanation: string }[];
|
|
}[];
|
|
guidance_version: string;
|
|
};
|
|
|
|
export default function FintechInvestment({
|
|
data,
|
|
caseId,
|
|
version,
|
|
canWrite,
|
|
documents,
|
|
simulator,
|
|
fromArticle,
|
|
}: {
|
|
data: FintechView;
|
|
caseId: string;
|
|
version: number;
|
|
canWrite: boolean;
|
|
documents: { document_id: string; file_name: string }[];
|
|
simulator: string | null;
|
|
fromArticle: boolean;
|
|
}) {
|
|
const form = useForm<{ version: number; profile: Profile }>({
|
|
version,
|
|
profile: {
|
|
model: data.record?.profile.model ?? 'undetermined',
|
|
fields: data.record?.profile.fields ?? {},
|
|
operation_countries: data.record?.profile.operation_countries ?? [],
|
|
activities: data.record?.profile.activities ?? {},
|
|
activity_basis: data.record?.profile.activity_basis ?? '',
|
|
documents: data.record?.profile.documents ?? {},
|
|
},
|
|
});
|
|
const errors = form.errors as Record<string, string>;
|
|
const errorSummary = useRef<HTMLDivElement>(null);
|
|
const set = <K extends keyof Profile>(key: K, value: Profile[K]) =>
|
|
form.setData('profile', { ...form.data.profile, [key]: value });
|
|
const errorLabel = (field: string) => {
|
|
if (field.startsWith('profile.fields.'))
|
|
return data.fields[field.slice(15)] ?? 'Caracterização';
|
|
if (field.startsWith('profile.documents.'))
|
|
return (
|
|
data.documents.find((d) => d.code === field.slice(18))?.label ??
|
|
'Documento'
|
|
);
|
|
if (field.startsWith('profile.operation_countries'))
|
|
return 'Países de operação declarados';
|
|
return (
|
|
(
|
|
{
|
|
version: 'Versão do dossier',
|
|
'profile.model': 'Natureza do modelo',
|
|
'profile.activity_basis': 'Fundamento das atividades',
|
|
} as Record<string, string>
|
|
)[field] ?? 'Caracterização'
|
|
);
|
|
};
|
|
const status: Record<string, string> = {
|
|
applicable: 'Aplicável à preparação do enquadramento',
|
|
dispensed: 'Dispensado deste levantamento, com fundamento declarado',
|
|
undetermined: 'Por determinar',
|
|
};
|
|
return (
|
|
<section
|
|
aria-labelledby="fintech-title"
|
|
className="space-y-4 rounded-xl border p-4"
|
|
>
|
|
<h2 id="fintech-title" className="text-xl font-semibold">
|
|
Caracterização Fintech no mesmo dossier
|
|
</h2>
|
|
<p>
|
|
Preparação incompleta é permitida. Os estados, as cinco fases e
|
|
as 19 etapas do percurso mantêm-se. A declaração do modelo não é
|
|
classificação jurídica nem licença.
|
|
</p>
|
|
<details open={!!data.record || fromArticle}>
|
|
<summary className="cursor-pointer underline">
|
|
Preparar ou consultar este investimento como Fintech
|
|
</summary>
|
|
{data.record && (
|
|
<p className="my-3 text-sm">
|
|
Caracterização recuperada · {data.record.saved_at}
|
|
</p>
|
|
)}
|
|
{!!data.affected_reviews.length && (
|
|
<p role="status">
|
|
Rever apenas {data.affected_reviews.join(', ')}: os
|
|
pressupostos desse âmbito mudaram. Os pareceres
|
|
anteriores permanecem no histórico.
|
|
</p>
|
|
)}
|
|
<form
|
|
className="mt-4 space-y-5"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
form.transform((value) => ({ ...value, version }));
|
|
form.post(`/investimentos/${caseId}/fintech`, {
|
|
preserveScroll: true,
|
|
onError: () =>
|
|
requestAnimationFrame(() =>
|
|
errorSummary.current?.focus(),
|
|
),
|
|
});
|
|
}}
|
|
>
|
|
{!!Object.keys(errors).length && (
|
|
<div
|
|
ref={errorSummary}
|
|
tabIndex={-1}
|
|
role="alert"
|
|
className="rounded border border-red-700 p-3"
|
|
>
|
|
<p>
|
|
Não foi possível guardar. Os dados preenchidos
|
|
foram preservados.
|
|
</p>
|
|
<ul>
|
|
{Object.entries(errors).map(
|
|
([field, message]) => (
|
|
<li key={field}>
|
|
{errorLabel(field)}: {message}
|
|
</li>
|
|
),
|
|
)}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
<fieldset
|
|
disabled={!canWrite || form.processing}
|
|
className="space-y-4 disabled:opacity-80"
|
|
>
|
|
<legend className="font-semibold">
|
|
Modelo declarado
|
|
</legend>
|
|
<label className="grid gap-1">
|
|
Natureza do modelo
|
|
<select
|
|
className="w-full rounded border p-2"
|
|
value={form.data.profile.model}
|
|
onChange={(e) => set('model', e.target.value)}
|
|
>
|
|
<option value="undetermined">
|
|
Por determinar
|
|
</option>
|
|
<option value="technology_provider">
|
|
Fornecedor de tecnologia, sem presumir
|
|
atividade financeira reservada
|
|
</option>
|
|
<option value="financial_operator">
|
|
Operador de serviços financeiros declarado,
|
|
sujeito a enquadramento concreto
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{Object.entries(data.fields).map(([key, label]) => (
|
|
<label key={key} className="grid gap-1">
|
|
{label}
|
|
<textarea
|
|
rows={3}
|
|
maxLength={4000}
|
|
className="w-full min-w-0 rounded border p-2"
|
|
aria-invalid={
|
|
!!errors[`profile.fields.${key}`]
|
|
}
|
|
value={
|
|
form.data.profile.fields[key] ?? ''
|
|
}
|
|
onChange={(e) =>
|
|
set('fields', {
|
|
...form.data.profile.fields,
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
{errors[`profile.fields.${key}`] && (
|
|
<span role="alert">
|
|
{errors[`profile.fields.${key}`]}
|
|
</span>
|
|
)}
|
|
</label>
|
|
))}
|
|
</div>
|
|
<label className="grid gap-1">
|
|
Países de operação declarados
|
|
<select
|
|
multiple
|
|
className="h-36 w-full rounded border p-2"
|
|
value={form.data.profile.operation_countries}
|
|
onChange={(e) =>
|
|
set(
|
|
'operation_countries',
|
|
Array.from(
|
|
e.target.selectedOptions,
|
|
(o) => o.value,
|
|
),
|
|
)
|
|
}
|
|
>
|
|
{data.countries.map((c) => (
|
|
<option key={c.code} value={c.code}>
|
|
{c.code} — {c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<p className="text-sm">
|
|
Estes países referem-se à operação. País mestre da
|
|
entidade, país declarado no processo, origem,
|
|
nacionalidade, residência, residência fiscal e
|
|
cambial mantêm os seus cadastros e significados.
|
|
Nenhuma classificação regulatória é inferida daqui.
|
|
</p>
|
|
<fieldset className="grid gap-3 md:grid-cols-2">
|
|
<legend className="font-semibold">
|
|
Atividades pretendidas, ainda sujeitas a
|
|
enquadramento
|
|
</legend>
|
|
{Object.entries(data.activities).map(
|
|
([key, label]) => (
|
|
<label key={key} className="grid gap-1">
|
|
{label}
|
|
<select
|
|
className="rounded border p-2"
|
|
value={
|
|
form.data.profile.activities[
|
|
key
|
|
] ?? 'unknown'
|
|
}
|
|
onChange={(e) =>
|
|
set('activities', {
|
|
...form.data.profile
|
|
.activities,
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="unknown">
|
|
Por determinar
|
|
</option>
|
|
<option value="no">
|
|
Não previsto neste modelo
|
|
</option>
|
|
<option value="yes">
|
|
Previsto neste modelo
|
|
</option>
|
|
</select>
|
|
</label>
|
|
),
|
|
)}
|
|
</fieldset>
|
|
<label className="grid gap-1">
|
|
Fundamento e limites da declaração de atividades
|
|
<textarea
|
|
rows={3}
|
|
maxLength={3000}
|
|
className="rounded border p-2"
|
|
value={form.data.profile.activity_basis}
|
|
onChange={(e) =>
|
|
set('activity_basis', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<fieldset className="grid gap-3">
|
|
<legend className="font-semibold">
|
|
Documentação por etapa e âmbito
|
|
</legend>
|
|
<p>
|
|
Não é obrigatório anexar tudo para criar ou
|
|
preparar a ideia. Selecione documentos já
|
|
associados ao dossier; a associação não publica
|
|
o ficheiro.
|
|
</p>
|
|
{data.documents.map((d) => (
|
|
<label key={d.code} className="grid gap-1">
|
|
{d.stage} · {d.label}
|
|
<select
|
|
className="w-full rounded border p-2"
|
|
aria-invalid={
|
|
!!errors[
|
|
`profile.documents.${d.code}`
|
|
]
|
|
}
|
|
value={
|
|
form.data.profile.documents[
|
|
d.code
|
|
] ?? ''
|
|
}
|
|
onChange={(e) =>
|
|
set('documents', {
|
|
...form.data.profile.documents,
|
|
[d.code]: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">
|
|
Ainda não associado
|
|
</option>
|
|
{documents.map((doc) => (
|
|
<option
|
|
key={doc.document_id}
|
|
value={doc.document_id}
|
|
>
|
|
{doc.file_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="text-sm">{d.action}</span>
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
{canWrite && (
|
|
<button
|
|
type="submit"
|
|
disabled={form.processing}
|
|
className="rounded border px-4 py-2"
|
|
>
|
|
{form.processing
|
|
? 'A guardar…'
|
|
: 'Guardar caracterização incompleta'}
|
|
</button>
|
|
)}
|
|
</fieldset>
|
|
{form.recentlySuccessful && (
|
|
<p role="status">
|
|
Caracterização guardada. Continue a preparação nas
|
|
etapas e ferramentas deste dossier.
|
|
</p>
|
|
)}
|
|
{!canWrite && (
|
|
<p>
|
|
Consulta autorizada. A preparação destes campos cabe
|
|
ao titular; a conferência independente usa os
|
|
controlos de etapa existentes.
|
|
</p>
|
|
)}
|
|
</form>
|
|
<section
|
|
aria-label="Aplicabilidade declarada Fintech"
|
|
className="mt-5 space-y-3"
|
|
>
|
|
<h3 className="font-semibold">
|
|
Aplicabilidade por atividade e ação
|
|
</h3>
|
|
<p>
|
|
Quem prepara: promotor. Quem confere: responsável
|
|
interno independente. Quem decide externamente: entidade
|
|
competente identificada no ato, quando aplicável.
|
|
</p>
|
|
<ul className="space-y-3">
|
|
{data.requirements.map((q) => (
|
|
<li key={q.code} className="rounded border p-3">
|
|
<p>
|
|
{q.activity} · {status[q.state]}
|
|
</p>
|
|
<p className="text-sm">{q.basis}</p>
|
|
<p className="text-sm">
|
|
Disposição, versão, período, licença,
|
|
capital, taxa e prazo específicos: por
|
|
determinar. A preparação independente pode
|
|
continuar.
|
|
</p>
|
|
<p className="text-sm">{q.limit}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
<section
|
|
className="mt-5 space-y-2"
|
|
aria-label="Preparação operacional Fintech"
|
|
>
|
|
<h3 className="font-semibold">
|
|
Recursos e preparação da operação
|
|
</h3>
|
|
<p>
|
|
Registe investimento, capital próprio, financiamento
|
|
assegurado, necessidade e pressupostos na ferramenta
|
|
financeira existente. Dados desconhecidos permanecem
|
|
indeterminados; não são zero.
|
|
</p>
|
|
{simulator && (
|
|
<Link className="underline" href={simulator}>
|
|
Preparar o orçamento e recuperar o resultado neste
|
|
dossier
|
|
</Link>
|
|
)}
|
|
<p>
|
|
<a href="#S16" className="underline">
|
|
S16 · Implementação
|
|
</a>{' '}
|
|
·{' '}
|
|
<a href="#S17" className="underline">
|
|
S17 · Testes e preparação operacional
|
|
</a>{' '}
|
|
·{' '}
|
|
<a href="#S18" className="underline">
|
|
S18 · Início de operação
|
|
</a>{' '}
|
|
·{' '}
|
|
<a href="#S19" className="underline">
|
|
S19 · Acompanhamento
|
|
</a>
|
|
</p>
|
|
<p>
|
|
Guardar um marco preparatório ou uma conferência interna
|
|
não comprova autorização institucional. Use o ato
|
|
externo, documento e âmbito quando existirem; não há
|
|
integração automática com instituições.
|
|
</p>
|
|
</section>
|
|
<section className="mt-4">
|
|
<h3 className="font-semibold">
|
|
Orientação preparatória existente
|
|
</h3>
|
|
<ul>
|
|
{data.guidance.map((item) => (
|
|
<li key={item.stage}>
|
|
{item.stage}:{' '}
|
|
{item.recommendations
|
|
.map((r) => r.explanation)
|
|
.join(' ')}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="text-sm">
|
|
Versão: {data.guidance_version}. As recomendações não
|
|
são decisões externas e não criam outra sequência de
|
|
etapas.
|
|
</p>
|
|
</section>
|
|
<ul className="mt-4 text-sm">
|
|
{data.limits.map((limit) => (
|
|
<li key={limit}>{limit}</li>
|
|
))}
|
|
</ul>
|
|
</details>
|
|
</section>
|
|
);
|
|
}
|