Files
Investidor-V2/resources/js/components/fintech-investment.tsx
T
nala 6a76aac80f
tests / ci (push) Canceled after 0s
versao compilada 1
2026-10-16 09:17:28 +02:00

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>
);
}