Wire grounded private investment preparation into QA dossier path
This commit is contained in:
@@ -1,24 +1,232 @@
|
||||
import {
|
||||
PreparationSummary,
|
||||
type Preparation,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
type Snapshot = {context_revision:number;path?:{definition_version:number;steps:Array<{code:string;title:string;applicability:string;reason:string;state:string;context_review_required?:boolean}>}; zone?:{title:string;href:string;version:number|string;fields?:Record<string,string>;analysis?:{conditions?:Array<{id:string;label:string;knowledge:string;source_reference?:string;valid_from?:string;valid_until?:string}>;source?:{title:string;url:string;diploma?:string;article?:string;valid_from?:string;valid_until?:string}|null}}|null;fields?:Record<string,string>};
|
||||
export function InvestmentContextAssessment({raw,contextId}:{raw:unknown;contextId?:string|null}) {
|
||||
let snapshot:Snapshot|null=null;
|
||||
try {const value=JSON.parse(String(raw ?? ''));if(value.version===1) snapshot=value;} catch { /* Missing context remains explicit. */ }
|
||||
if(!snapshot) return null;
|
||||
const fields=snapshot.fields ?? {};const zone=snapshot.zone;
|
||||
const regime=zone?.fields?.investment_regime ?? fields.investment_regime;
|
||||
const names:Record<string,string>={zee:'ZEE',zfi:'ZFI',industrial_park:'Parque industrial'};
|
||||
return <section className="space-y-2 rounded border border-amber-400 p-3" aria-label="Aplicabilidade ao investimento">
|
||||
<h3 className="font-semibold">Aplicabilidade ao investimento · contexto v{snapshot.context_revision}</h3>
|
||||
<p>Origem do investidor: {{national:'nacional',foreign:'estrangeiro',mixed:'misto'}[fields.investor_origin] ?? 'informação em falta'}. Residência fiscal declarada: {fields.tax_residency_country || 'informação em falta'}. Estas condições são distintas; uma não determina a outra.</p>
|
||||
{zone ? <><p><strong>Potencialmente aplicável — análise específica de {names[regime ?? ''] ?? 'zona seleccionada'}.</strong> <Link className="underline" href={zone.href}>{zone.title} · versão {zone.version} · consultar ficha e fundamentos</Link>.</p>
|
||||
<p>A selecção identifica a referência consultada; não confirma o reconhecimento da entidade, actividade autorizada, licença ou acesso a benefícios. Consulte as condições documentadas na ficha e reúna a autorização ou evidência correspondente no dossier.</p>
|
||||
{zone.analysis?.source && <p>Fundamento cadastrado: <a className="underline" href={zone.analysis.source.url}>{zone.analysis.source.diploma || zone.analysis.source.title} {zone.analysis.source.article}</a>. Período cadastrado: {zone.analysis.source.valid_from || 'início por indicar'} a {zone.analysis.source.valid_until || 'sem termo indicado'}. A cobertura do investimento exige correspondência com estas condições.</p>}
|
||||
{zone.analysis?.conditions?.length ? <ul className="list-disc pl-5">{zone.analysis.conditions.map(condition=><li key={condition.id}><strong>{condition.label}</strong>: {condition.knowledge==='documented'?'condição documentada; cumprimento pelo investimento por confirmar':condition.knowledge==='disputed'?'condição com informação divergente':'informação em falta'}. {condition.source_reference && <>Fundamento: {condition.source_reference}.</>} {condition.valid_from && <>Desde {condition.valid_from}.</>} {condition.valid_until && <>Até {condition.valid_until}.</>}</li>)}</ul> : <p>Condições específicas: informação estruturada em falta nesta referência. A ficha e os documentos continuam consultáveis; não se presume cumprimento.</p>}
|
||||
<p role="status">Benefício fiscal deste enquadramento: indeterminado. Não existe neste consumidor uma regra executável ligada ao reconhecimento e condições da zona. No simulador, o IVA disponível corresponde apenas ao cenário geral sem benefício, sobre vendas declaradas compatíveis; não simula o regime especial. O cenário com benefício e o apuramento final estão indisponíveis.</p></>
|
||||
: <p>Enquadramento geral de preparação: sem ZEE, ZFI ou parque seleccionado. Isso não confirma a elegibilidade para uma operação fiscal. O cálculo mantém o âmbito e as exclusões da versão publicada.</p>}
|
||||
<p>Confirmado neste quadro significa apenas dado recuperado do dossier; declarações e referências não são decisões jurídicas. Requisitos específicos não documentados permanecem indeterminados.</p>
|
||||
{snapshot.path && <details><summary>Passos adaptados pelo percurso existente · versão {snapshot.path.definition_version}</summary><ul className="list-disc pl-5">{snapshot.path.steps.map(step=><li key={step.code}>{step.code} — {step.title}: {step.context_review_required?'informação alterada; rever fundamento':step.applicability==='not_needed_for_preparation'?'não aplicável à preparação declarada':step.applicability==='undetermined'?'informação em falta':['completed','approved'].includes(step.state)?'preparação confirmada no percurso; não é decisão fiscal':'potencialmente aplicável'}. {step.reason}</li>)}</ul></details>}
|
||||
{contextId && <Link className="underline" href={`/investimentos/${contextId}/contexto/editar`}>Alterar contexto e comparar impacto antes de guardar</Link>}
|
||||
</section>;
|
||||
type Snapshot = {
|
||||
preparation?: Preparation | null;
|
||||
context_revision: number;
|
||||
path?: {
|
||||
definition_version: number;
|
||||
steps: Array<{
|
||||
code: string;
|
||||
title: string;
|
||||
applicability: string;
|
||||
reason: string;
|
||||
state: string;
|
||||
context_review_required?: boolean;
|
||||
}>;
|
||||
};
|
||||
zone?: {
|
||||
title: string;
|
||||
href: string;
|
||||
version: number | string;
|
||||
fields?: Record<string, string>;
|
||||
analysis?: {
|
||||
conditions?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
knowledge: string;
|
||||
source_reference?: string;
|
||||
valid_from?: string;
|
||||
valid_until?: string;
|
||||
}>;
|
||||
source?: {
|
||||
title: string;
|
||||
url: string;
|
||||
diploma?: string;
|
||||
article?: string;
|
||||
valid_from?: string;
|
||||
valid_until?: string;
|
||||
} | null;
|
||||
};
|
||||
} | null;
|
||||
fields?: Record<string, string>;
|
||||
};
|
||||
export function InvestmentContextAssessment({
|
||||
raw,
|
||||
contextId,
|
||||
}: {
|
||||
raw: unknown;
|
||||
contextId?: string | null;
|
||||
}) {
|
||||
let snapshot: Snapshot | null = null;
|
||||
try {
|
||||
const value = JSON.parse(String(raw ?? ''));
|
||||
if (value.version === 1) snapshot = value;
|
||||
} catch {
|
||||
/* Missing context remains explicit. */
|
||||
}
|
||||
if (!snapshot) return null;
|
||||
const fields = snapshot.fields ?? {};
|
||||
const zone = snapshot.zone;
|
||||
const regime = zone?.fields?.investment_regime ?? fields.investment_regime;
|
||||
const names: Record<string, string> = {
|
||||
zee: 'ZEE',
|
||||
zfi: 'ZFI',
|
||||
industrial_park: 'Parque industrial',
|
||||
};
|
||||
return (
|
||||
<section
|
||||
className="space-y-2 rounded border border-amber-400 p-3"
|
||||
aria-label="Aplicabilidade ao investimento"
|
||||
>
|
||||
<h3 className="font-semibold">
|
||||
Aplicabilidade ao investimento · contexto v
|
||||
{snapshot.context_revision}
|
||||
</h3>
|
||||
<p>
|
||||
Origem do investidor:{' '}
|
||||
{{
|
||||
national: 'nacional',
|
||||
foreign: 'estrangeiro',
|
||||
mixed: 'misto',
|
||||
}[fields.investor_origin] ?? 'informação em falta'}
|
||||
. Residência fiscal declarada:{' '}
|
||||
{fields.tax_residency_country || 'informação em falta'}. Estas
|
||||
condições são distintas; uma não determina a outra.
|
||||
</p>
|
||||
{zone ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>
|
||||
Potencialmente aplicável — análise específica de{' '}
|
||||
{names[regime ?? ''] ?? 'zona seleccionada'}.
|
||||
</strong>{' '}
|
||||
<Link className="underline" href={zone.href}>
|
||||
{zone.title} · versão {zone.version} · consultar
|
||||
ficha e fundamentos
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
A selecção identifica a referência consultada; não
|
||||
confirma o reconhecimento da entidade, actividade
|
||||
autorizada, licença ou acesso a benefícios. Consulte as
|
||||
condições documentadas na ficha e reúna a autorização ou
|
||||
evidência correspondente no dossier.
|
||||
</p>
|
||||
{zone.analysis?.source && (
|
||||
<p>
|
||||
Fundamento cadastrado:{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={zone.analysis.source.url}
|
||||
>
|
||||
{zone.analysis.source.diploma ||
|
||||
zone.analysis.source.title}{' '}
|
||||
{zone.analysis.source.article}
|
||||
</a>
|
||||
. Período cadastrado:{' '}
|
||||
{zone.analysis.source.valid_from ||
|
||||
'início por indicar'}{' '}
|
||||
a{' '}
|
||||
{zone.analysis.source.valid_until ||
|
||||
'sem termo indicado'}
|
||||
. A cobertura do investimento exige correspondência
|
||||
com estas condições.
|
||||
</p>
|
||||
)}
|
||||
{zone.analysis?.conditions?.length ? (
|
||||
<ul className="list-disc pl-5">
|
||||
{zone.analysis.conditions.map((condition) => (
|
||||
<li key={condition.id}>
|
||||
<strong>{condition.label}</strong>:{' '}
|
||||
{condition.knowledge === 'documented'
|
||||
? 'condição documentada; cumprimento pelo investimento por confirmar'
|
||||
: condition.knowledge === 'disputed'
|
||||
? 'condição com informação divergente'
|
||||
: 'informação em falta'}
|
||||
.{' '}
|
||||
{condition.source_reference && (
|
||||
<>
|
||||
Fundamento:{' '}
|
||||
{condition.source_reference}.
|
||||
</>
|
||||
)}{' '}
|
||||
{condition.valid_from && (
|
||||
<>Desde {condition.valid_from}.</>
|
||||
)}{' '}
|
||||
{condition.valid_until && (
|
||||
<>Até {condition.valid_until}.</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>
|
||||
Condições específicas: informação estruturada em
|
||||
falta nesta referência. A ficha e os documentos
|
||||
continuam consultáveis; não se presume cumprimento.
|
||||
</p>
|
||||
)}
|
||||
<p role="status">
|
||||
Benefício fiscal deste enquadramento: indeterminado. Não
|
||||
existe neste consumidor uma regra executável ligada ao
|
||||
reconhecimento e condições da zona. No simulador, o IVA
|
||||
disponível corresponde apenas ao cenário geral sem
|
||||
benefício, sobre vendas declaradas compatíveis; não
|
||||
simula o regime especial. O cenário com benefício e o
|
||||
apuramento final estão indisponíveis.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Enquadramento geral de preparação: sem ZEE, ZFI ou parque
|
||||
seleccionado. Isso não confirma a elegibilidade para uma
|
||||
operação fiscal. O cálculo mantém o âmbito e as exclusões da
|
||||
versão publicada.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
Confirmado neste quadro significa apenas dado recuperado do
|
||||
dossier; declarações e referências não são decisões jurídicas.
|
||||
Requisitos específicos não documentados permanecem
|
||||
indeterminados.
|
||||
</p>
|
||||
{snapshot.path && (
|
||||
<details>
|
||||
<summary>
|
||||
Passos adaptados pelo percurso existente · versão{' '}
|
||||
{snapshot.path.definition_version}
|
||||
</summary>
|
||||
<ul className="list-disc pl-5">
|
||||
{snapshot.path.steps.map((step) => (
|
||||
<li key={step.code}>
|
||||
{step.code} — {step.title}:{' '}
|
||||
{step.context_review_required
|
||||
? 'informação alterada; rever fundamento'
|
||||
: step.applicability ===
|
||||
'not_needed_for_preparation'
|
||||
? 'não aplicável à preparação declarada'
|
||||
: step.applicability === 'undetermined'
|
||||
? 'informação em falta'
|
||||
: ['completed', 'approved'].includes(
|
||||
step.state,
|
||||
)
|
||||
? 'preparação confirmada no percurso; não é decisão fiscal'
|
||||
: 'potencialmente aplicável'}
|
||||
. {step.reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
{snapshot.preparation?.enabled && (
|
||||
<PreparationSummary
|
||||
data={snapshot.preparation}
|
||||
caseId={contextId ?? undefined}
|
||||
/>
|
||||
)}
|
||||
{contextId && (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${contextId}/contexto/editar`}
|
||||
>
|
||||
Alterar contexto e comparar impacto antes de guardar
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { card, input } from '@/pages/investment/shared';
|
||||
|
||||
export type PreparationDocument = {
|
||||
document_id: string;
|
||||
file_name: string;
|
||||
status: string;
|
||||
malware_scan_status: string;
|
||||
};
|
||||
export type Preparation = {
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
mode: string;
|
||||
period_covered: boolean;
|
||||
scope_covered: boolean;
|
||||
reason: string[];
|
||||
missing_fields: string[];
|
||||
missing_labels?: string[];
|
||||
authority: string;
|
||||
next: string;
|
||||
calculation_notice: string;
|
||||
notice: string;
|
||||
source: { url: string; sha256: string };
|
||||
authority_source: { reference: string; url: string };
|
||||
questions: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
options?: Record<string, string>;
|
||||
when?: { field: string; equals: string };
|
||||
source: string;
|
||||
help?: string;
|
||||
}>;
|
||||
requirements: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
state: string;
|
||||
required: boolean;
|
||||
document_id: string | null;
|
||||
document: PreparationDocument | null;
|
||||
stage: string;
|
||||
reason: string;
|
||||
source: { reference: string; url: string; period: string };
|
||||
}>;
|
||||
};
|
||||
const states: Record<string, string> = {
|
||||
not_applicable: 'Não aplicável neste contexto',
|
||||
indeterminate: 'Informação em falta para decidir',
|
||||
missing: 'Documento por associar',
|
||||
associated: 'Documento associado — conteúdo por conferir',
|
||||
};
|
||||
export const preparationMode = (mode: string) =>
|
||||
({
|
||||
authorization: 'Pedido de autorização',
|
||||
registration: 'Pedido de mero registo',
|
||||
indeterminate: 'Enquadramento por determinar',
|
||||
})[mode] ?? mode;
|
||||
|
||||
export function PreparationSummary({
|
||||
data,
|
||||
caseId,
|
||||
}: {
|
||||
data: Preparation | null | undefined;
|
||||
caseId?: string;
|
||||
}) {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<section
|
||||
id="private-investment-preparation"
|
||||
className={card + ' space-y-3'}
|
||||
aria-label="Preparação adaptada do investimento privado"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Preparar o pedido de investimento privado
|
||||
</h2>
|
||||
{!data.enabled ? (
|
||||
<p>
|
||||
Use os dados deste dossier para identificar o pedido e os
|
||||
documentos aplicáveis. A preparação é opcional e não atribui
|
||||
benefícios.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<strong>{preparationMode(data.mode)}</strong> · recorte
|
||||
de 2026 ·{' '}
|
||||
{data.version.replace(
|
||||
'private-investment-preparation-',
|
||||
'',
|
||||
)}
|
||||
</p>
|
||||
<p>{data.next}</p>
|
||||
{data.missing_fields.length > 0 && (
|
||||
<p>
|
||||
Informação em falta:{' '}
|
||||
{(
|
||||
data.missing_labels ??
|
||||
data.missing_fields.map(
|
||||
(key) =>
|
||||
data.questions?.find(
|
||||
(q) => q.key === key,
|
||||
)?.label ??
|
||||
(
|
||||
{
|
||||
currency: 'Moeda do projecto',
|
||||
expected_start_date:
|
||||
'Data prevista de início',
|
||||
entity_stage:
|
||||
'Situação da entidade',
|
||||
} as Record<string, string>
|
||||
)[key] ??
|
||||
key,
|
||||
)
|
||||
).join('; ')}
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{!data.period_covered && (
|
||||
<p>
|
||||
Este incremento cobre pedidos com início previsto em
|
||||
2026. Uma data em falta ou fora desse recorte não
|
||||
permite determinar o pedido por este lote; os
|
||||
restantes trabalhos do dossier continuam
|
||||
disponíveis.
|
||||
</p>
|
||||
)}
|
||||
{!data.scope_covered && (
|
||||
<p>
|
||||
Declare a actividade efectiva. Prospecção,
|
||||
pesquisa/produção extractiva e investimento público
|
||||
ou exclusivamente social/sem fins lucrativos seguem
|
||||
enquadramentos próprios, fora deste lote.
|
||||
</p>
|
||||
)}
|
||||
{data.reason.map((reason) => (
|
||||
<p key={reason}>{reason}</p>
|
||||
))}
|
||||
<p>
|
||||
Encaminhamento indicativo: {data.authority}.{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={data.authority_source.url}
|
||||
>
|
||||
{data.authority_source.reference}
|
||||
</a>
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{data.requirements
|
||||
.filter((r) => r.state !== 'not_applicable')
|
||||
.map((r) => (
|
||||
<li key={r.key} className="rounded border p-3">
|
||||
<strong>{r.label}</strong> —{' '}
|
||||
{states[r.state]}.{' '}
|
||||
{r.document?.file_name && (
|
||||
<span>
|
||||
Ficheiro: {r.document.file_name}
|
||||
.{' '}
|
||||
</span>
|
||||
)}
|
||||
<p>
|
||||
{r.reason}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
Ver fundamento: {r.source.reference}
|
||||
</a>{' '}
|
||||
· {r.source.period}.
|
||||
</p>
|
||||
{caseId && (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${caseId}/contexto/editar#preparation-documents`}
|
||||
>
|
||||
Rever dados ou associar documento do
|
||||
dossier
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<details>
|
||||
<summary>
|
||||
Exigências que não se aplicam — histórico preservado
|
||||
</summary>
|
||||
{data.requirements
|
||||
.filter((r) => r.state === 'not_applicable')
|
||||
.map((r) => (
|
||||
<p key={r.key}>
|
||||
{r.label}: {r.reason}{' '}
|
||||
{r.document_id && (
|
||||
<>
|
||||
Associação anterior mantida
|
||||
{r.document?.file_name
|
||||
? `: ${r.document.file_name}`
|
||||
: ''}
|
||||
.
|
||||
</>
|
||||
)}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</details>
|
||||
<p>{data.calculation_notice}</p>
|
||||
<p>{data.notice}</p>
|
||||
</>
|
||||
)}
|
||||
<a className="underline" href={data.source.url}>
|
||||
Consultar o exemplar oficial e os fundamentos
|
||||
</a>
|
||||
{caseId && (
|
||||
<p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${caseId}/contexto/editar#preparation-questions`}
|
||||
>
|
||||
Completar apenas os dados em falta ou editar declarações
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreparationEditor({
|
||||
data,
|
||||
fields,
|
||||
changes,
|
||||
documents,
|
||||
links,
|
||||
onChange,
|
||||
onLink,
|
||||
disabled,
|
||||
}: {
|
||||
data: Preparation;
|
||||
fields: Record<string, string>;
|
||||
changes: Record<string, string>;
|
||||
documents: PreparationDocument[];
|
||||
links: Record<string, string | null>;
|
||||
onChange: (key: string, value: string) => void;
|
||||
onLink: (key: string, value: string | null) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const values = { ...fields, ...changes };
|
||||
const active = values.investment_preparation === 'yes';
|
||||
const excluded = ['extractive', 'public_nonprofit'].includes(
|
||||
values.investment_activity_scope,
|
||||
);
|
||||
const applicable = (q: Preparation['questions'][number]) =>
|
||||
!q.when || values[q.when.field] === q.when.equals;
|
||||
// Keep a field in place while the user edits it; collapse only recovered answers.
|
||||
const known = (q: Preparation['questions'][number]) =>
|
||||
!!fields[q.key] && fields[q.key] !== 'unknown';
|
||||
const render = (q: Preparation['questions'][number]) => (
|
||||
<label className="grid gap-1" key={q.key} htmlFor={'prep-' + q.key}>
|
||||
{q.label}
|
||||
{q.options ? (
|
||||
<select
|
||||
id={'prep-' + q.key}
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
value={values[q.key] ?? ''}
|
||||
onChange={(e) => onChange(q.key, e.target.value)}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{Object.entries(q.options).map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
id={'prep-' + q.key}
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={values[q.key] ?? ''}
|
||||
onChange={(e) => onChange(q.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{q.help} Fundamento: {q.source}.
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
const questions = data.questions.filter(
|
||||
(q) =>
|
||||
q.key !== 'investment_preparation' &&
|
||||
applicable(q) &&
|
||||
(!excluded || q.key === 'investment_activity_scope'),
|
||||
);
|
||||
const needed = questions.filter(
|
||||
(q) =>
|
||||
!known(q) &&
|
||||
(!data.enabled ||
|
||||
data.mode !== 'authorization' ||
|
||||
data.missing_fields.includes(q.key)),
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="preparation-questions"
|
||||
className={card + ' space-y-4'}
|
||||
aria-label="Dados do pedido de investimento privado"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Dados em falta para preparar o pedido
|
||||
</h2>
|
||||
<p>
|
||||
Valores conhecidos são reutilizados. As alterações só são
|
||||
gravadas depois da comparação de impacto. Nacionalidade e
|
||||
residência não determinam a origem do capital nem a modalidade
|
||||
da empresa.
|
||||
</p>
|
||||
{render(data.questions[0])}
|
||||
{active && (
|
||||
<>
|
||||
{needed.map(render)}
|
||||
<details>
|
||||
<summary>
|
||||
Dados já declarados — consultar ou alterar
|
||||
</summary>
|
||||
<div className="space-y-3">
|
||||
{questions.filter(known).map(render)}
|
||||
</div>
|
||||
</details>
|
||||
<p>
|
||||
Data, moeda e situação da entidade usam as declarações
|
||||
acima.{' '}
|
||||
{data.missing_fields.filter((k) =>
|
||||
[
|
||||
'currency',
|
||||
'expected_start_date',
|
||||
'entity_stage',
|
||||
].includes(k),
|
||||
).length > 0 &&
|
||||
'Complete esses campos em falta antes de comparar.'}
|
||||
</p>
|
||||
<div id="preparation-documents" className="space-y-3">
|
||||
<h3 className="font-bold">
|
||||
Associar documentos já existentes no dossier
|
||||
</h3>
|
||||
<p>
|
||||
Carregue documentos pelo Balcão Virtual do mesmo
|
||||
dossier. A associação não aprova o conteúdo nem
|
||||
substitui a análise do pedido.
|
||||
</p>
|
||||
{data.requirements
|
||||
.filter((r) => r.required)
|
||||
.map((r) => (
|
||||
<label className="grid gap-1" key={r.key}>
|
||||
{r.label}
|
||||
<select
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
value={
|
||||
Object.hasOwn(links, r.key)
|
||||
? (links[r.key] ?? '')
|
||||
: (r.document_id ?? '')
|
||||
}
|
||||
onChange={(e) =>
|
||||
onLink(
|
||||
r.key,
|
||||
e.target.value || null,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Por associar</option>
|
||||
{documents.map((doc) => (
|
||||
<option
|
||||
key={doc.document_id}
|
||||
value={doc.document_id}
|
||||
>
|
||||
{doc.file_name} ·{' '}
|
||||
{{
|
||||
quarantined:
|
||||
'Em quarentena',
|
||||
active: 'Disponível',
|
||||
deleted: 'Removido',
|
||||
}[doc.status] ??
|
||||
'Estado por conferir'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
{r.reason}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
Compare o impacto para actualizar o enquadramento, as perguntas
|
||||
e as exigências. Uma exigência que deixe de ser aplicável
|
||||
conserva o documento anterior no histórico.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreparationImpact({
|
||||
before,
|
||||
after,
|
||||
}: {
|
||||
before: Preparation;
|
||||
after: Preparation;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="space-y-2 rounded border border-amber-400 p-3"
|
||||
aria-label="Impacto no pedido de investimento"
|
||||
>
|
||||
<h3 className="font-bold">Impacto no pedido e nos documentos</h3>
|
||||
<p>
|
||||
{preparationMode(before.mode)} → {preparationMode(after.mode)}.
|
||||
</p>
|
||||
<p>Próximo passo: {after.next}.</p>
|
||||
<ul className="list-disc pl-5">
|
||||
{after.requirements
|
||||
.filter(
|
||||
(r) =>
|
||||
before.requirements.find((b) => b.key === r.key)
|
||||
?.state !== r.state,
|
||||
)
|
||||
.map((r) => (
|
||||
<li key={r.key}>
|
||||
{r.label}:{' '}
|
||||
{
|
||||
states[
|
||||
before.requirements.find(
|
||||
(b) => b.key === r.key,
|
||||
)?.state ?? 'indeterminate'
|
||||
]
|
||||
}{' '}
|
||||
→ {states[r.state]}.{' '}
|
||||
{r.document_id && 'Vínculo documental preservado.'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{after.calculation_notice}</p>
|
||||
<PreparationSummary data={after} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
Preparation,
|
||||
PreparationDocument,
|
||||
} from '@/components/private-investment-preparation';
|
||||
function csrfToken(): string {
|
||||
return decodeURIComponent(
|
||||
document.cookie
|
||||
@@ -56,6 +60,8 @@ export type DossierContext = {
|
||||
dependency_hash?: string;
|
||||
};
|
||||
export type ContextView = {
|
||||
private_investment?: Preparation | null;
|
||||
preparation_documents?: PreparationDocument[];
|
||||
simulator_context_revision?: number | null;
|
||||
case: { id: string; title: string; version: number };
|
||||
context: DossierContext;
|
||||
@@ -73,6 +79,7 @@ export type ContextView = {
|
||||
notice: string;
|
||||
};
|
||||
export type ContextPlan = {
|
||||
private_investment?: { before: Preparation; after: Preparation } | null;
|
||||
preview_hash: string;
|
||||
version: number;
|
||||
revision: number;
|
||||
@@ -119,6 +126,20 @@ export async function contextApi<T>(
|
||||
return data as T;
|
||||
}
|
||||
export const contextFieldNames: Record<string, string> = {
|
||||
investment_form: 'Tipo do investimento',
|
||||
investment_preparation: 'Preparação do pedido',
|
||||
investment_activity_scope: 'Natureza da actividade',
|
||||
implementation_mode: 'Modalidade da empresa',
|
||||
capital_origin: 'Origem dos capitais',
|
||||
site_tenure: 'Utilização das instalações',
|
||||
project_concession: 'PPP ou concessão empresarial',
|
||||
significant_implications: 'Implicações significativas',
|
||||
land_extension: 'Extensão de terra',
|
||||
forest_concession: 'Concessão florestal',
|
||||
forest_concession_hectares: 'Área florestal em hectares',
|
||||
total_investment: 'Valor total do investimento',
|
||||
area_required: 'Área de terra requerida',
|
||||
area_unit: 'Unidade da área',
|
||||
title: 'Título do investimento',
|
||||
territory_id: 'Localização documentada',
|
||||
location_source: 'Origem da localização',
|
||||
|
||||
@@ -279,15 +279,75 @@ function makeSections(
|
||||
|
||||
if (draft.context_applicability_snapshot) {
|
||||
try {
|
||||
const context = JSON.parse(String(draft.context_applicability_snapshot));
|
||||
const context = JSON.parse(
|
||||
String(draft.context_applicability_snapshot),
|
||||
);
|
||||
if (context.version === 1) {
|
||||
const section = sections.get('sources');
|
||||
const zone = context.zone;
|
||||
section?.rows.push({field:'Contexto do investimento',value:`Revisão ${context.context_revision}; ${zone ? `${zone.title}, versão ${zone.version}` : 'sem ZEE, ZFI ou parque seleccionado'}`,origin:'Referência do mesmo dossier no momento da gravação',classification:'Contexto versionado; não é reconhecimento jurídico'});
|
||||
section?.paragraphs.push(zone ? 'Benefício fiscal da zona indeterminado: falta regra executável ligada às condições e reconhecimento. O IVA apresentado é apenas cenário geral sem benefício; não representa o regime especial nem o apuramento final.' : 'Cenário geral de preparação; sem confirmação automática de elegibilidade fiscal.');
|
||||
if(zone?.href) section?.rows.push({field:'Ficha e fundamentos da zona',value:zone.href,origin:`Versão consultada ${zone.version}`,classification:'Referência consultável; condições por verificar'});
|
||||
section?.rows.push({
|
||||
field: 'Contexto do investimento',
|
||||
value: `Revisão ${context.context_revision}; ${zone ? `${zone.title}, versão ${zone.version}` : 'sem ZEE, ZFI ou parque seleccionado'}`,
|
||||
origin: 'Referência do mesmo dossier no momento da gravação',
|
||||
classification:
|
||||
'Contexto versionado; não é reconhecimento jurídico',
|
||||
});
|
||||
section?.paragraphs.push(
|
||||
zone
|
||||
? 'Benefício fiscal da zona indeterminado: falta regra executável ligada às condições e reconhecimento. O IVA apresentado é apenas cenário geral sem benefício; não representa o regime especial nem o apuramento final.'
|
||||
: 'Cenário geral de preparação; sem confirmação automática de elegibilidade fiscal.',
|
||||
);
|
||||
const preparation = context.preparation;
|
||||
if (preparation?.enabled) {
|
||||
const modes: Record<string, string> = {
|
||||
authorization: 'Pedido de autorização',
|
||||
registration: 'Pedido de mero registo',
|
||||
indeterminate: 'Enquadramento por determinar',
|
||||
};
|
||||
section?.rows.push({
|
||||
field: 'Preparação do investimento privado',
|
||||
value: modes[preparation.mode] ?? 'Por determinar',
|
||||
origin: `Contexto congelado v${context.context_revision}; ${preparation.version}`,
|
||||
classification:
|
||||
'Declarações e requisitos de preparação; sem aprovação jurídica',
|
||||
});
|
||||
section?.paragraphs.push(
|
||||
preparation.next,
|
||||
preparation.calculation_notice,
|
||||
preparation.notice,
|
||||
);
|
||||
for (const requirement of preparation.requirements ?? [])
|
||||
section?.rows.push({
|
||||
field: requirement.label,
|
||||
value:
|
||||
requirement.state === 'not_applicable'
|
||||
? 'Não aplicável; histórico preservado'
|
||||
: requirement.state === 'associated'
|
||||
? `Associado: ${requirement.document?.file_name ?? 'documento do dossier'}; conteúdo por conferir`
|
||||
: requirement.state === 'missing'
|
||||
? 'Documento por associar'
|
||||
: 'Informação em falta para decidir',
|
||||
origin: `${requirement.reason} ${requirement.source?.reference ?? ''}; ${requirement.source?.url ?? ''}`,
|
||||
classification:
|
||||
'Preparação informativa — simulação, sem submissão à Administração',
|
||||
});
|
||||
}
|
||||
if (zone?.href)
|
||||
section?.rows.push({
|
||||
field: 'Ficha e fundamentos da zona',
|
||||
value: zone.href,
|
||||
origin: `Versão consultada ${zone.version}`,
|
||||
classification:
|
||||
'Referência consultável; condições por verificar',
|
||||
});
|
||||
}
|
||||
} catch { sections.get('sources')?.paragraphs.push('Não foi possível recuperar o contexto guardado; o enquadramento territorial permanece indeterminado.'); }
|
||||
} catch {
|
||||
sections
|
||||
.get('sources')
|
||||
?.paragraphs.push(
|
||||
'Não foi possível recuperar o contexto guardado; o enquadramento territorial permanece indeterminado.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [sectionName, field, value, origin] of rows.slice(1)) {
|
||||
@@ -359,9 +419,23 @@ function makeSections(
|
||||
}
|
||||
|
||||
const finance = sections.get('finance');
|
||||
if (finance && ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(currentCalculation?.snapshot?.scenario_kind)) && currentCalculation?.snapshot) {
|
||||
for (const [field, value] of fiscalQaRows(currentCalculation.snapshot)) {
|
||||
finance.rows.push({ field, value, origin: 'Snapshot fiscal preservado; sem recálculo', classification: 'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar' });
|
||||
if (
|
||||
finance &&
|
||||
['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(
|
||||
String(currentCalculation?.snapshot?.scenario_kind),
|
||||
) &&
|
||||
currentCalculation?.snapshot
|
||||
) {
|
||||
for (const [field, value] of fiscalQaRows(
|
||||
currentCalculation.snapshot,
|
||||
)) {
|
||||
finance.rows.push({
|
||||
field,
|
||||
value,
|
||||
origin: 'Snapshot fiscal preservado; sem recálculo',
|
||||
classification:
|
||||
'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (finance && currentCalculation) {
|
||||
@@ -436,13 +510,22 @@ function makeSections(
|
||||
},
|
||||
{
|
||||
field: 'Fórmula e valores usados',
|
||||
value:
|
||||
['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind))
|
||||
? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.`
|
||||
: snapshot && typeof snapshot.formula_version === 'string'
|
||||
? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}`
|
||||
: 'A fórmula não foi identificada no snapshot; ver explicação da execução',
|
||||
origin: ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind)) ? (snapshot?.scenario_kind === 'fiscal_rule_published' ? 'Versão publicada na admissão; sem validação jurídica da operação' : 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação') : 'Snapshot guardado; sem aplicação de regra legal',
|
||||
value: [
|
||||
'fiscal_rule_qa_preview',
|
||||
'fiscal_rule_published',
|
||||
].includes(String(snapshot?.scenario_kind))
|
||||
? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.`
|
||||
: snapshot && typeof snapshot.formula_version === 'string'
|
||||
? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}`
|
||||
: 'A fórmula não foi identificada no snapshot; ver explicação da execução',
|
||||
origin: [
|
||||
'fiscal_rule_qa_preview',
|
||||
'fiscal_rule_published',
|
||||
].includes(String(snapshot?.scenario_kind))
|
||||
? snapshot?.scenario_kind === 'fiscal_rule_published'
|
||||
? 'Versão publicada na admissão; sem validação jurídica da operação'
|
||||
: 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação'
|
||||
: 'Snapshot guardado; sem aplicação de regra legal',
|
||||
classification: 'Reutilização do resultado existente',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
PreparationEditor,
|
||||
PreparationImpact,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import { InvestmentContextAssessment } from '@/components/investment-context-assessment';
|
||||
import ContextDocuments from '@/components/context-documents';
|
||||
import { Link } from '@inertiajs/react';
|
||||
@@ -36,6 +40,7 @@ export default function ContextEditor({
|
||||
}) {
|
||||
const navigation = useNavigationContext();
|
||||
const [view, setView] = useState(initial);
|
||||
const [preparation, setPreparation] = useState(initial.private_investment);
|
||||
const [options, setOptions] = useState(initialOptions);
|
||||
const [query, setQuery] = useState('');
|
||||
const [changes, setChanges] = useState<Partial<Record<Slot, string>>>(() =>
|
||||
@@ -49,6 +54,9 @@ export default function ContextEditor({
|
||||
const [declarations, setDeclarations] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [documentLinks, setDocumentLinks] = useState<
|
||||
Record<string, string | null>
|
||||
>({});
|
||||
const [replace, setReplace] = useState<string[]>([]);
|
||||
const [plan, setPlan] = useState<ContextPlan | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -59,6 +67,32 @@ export default function ContextEditor({
|
||||
string,
|
||||
unknown
|
||||
> | null>(null);
|
||||
const displayValue = (
|
||||
field: string,
|
||||
value: string | null | undefined,
|
||||
): string => {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return 'não informado';
|
||||
const question = view.private_investment?.questions.find(
|
||||
(q) => q.key === field,
|
||||
);
|
||||
return (
|
||||
question?.options?.[value] ??
|
||||
(
|
||||
{
|
||||
being_formed: 'Em constituição',
|
||||
existing: 'Existente',
|
||||
national: 'Nacional',
|
||||
foreign: 'Estrangeiro',
|
||||
mixed: 'Misto',
|
||||
yes: 'Sim',
|
||||
no: 'Não',
|
||||
unknown: 'Por determinar',
|
||||
} as Record<string, string>
|
||||
)[value] ??
|
||||
value
|
||||
);
|
||||
};
|
||||
const selected = (slot: Slot) =>
|
||||
Object.hasOwn(changes, slot)
|
||||
? (changes[slot] ?? '')
|
||||
@@ -77,6 +111,7 @@ export default function ContextEditor({
|
||||
selection: changes,
|
||||
declarations,
|
||||
replace_fields: replace,
|
||||
document_links: documentLinks,
|
||||
};
|
||||
try {
|
||||
const result = await contextApi<ContextPlan>(
|
||||
@@ -85,6 +120,9 @@ export default function ContextEditor({
|
||||
);
|
||||
setRequestBody(body);
|
||||
setPlan(result);
|
||||
setPreparation(
|
||||
result.private_investment?.after ?? view.private_investment,
|
||||
);
|
||||
} catch (e) {
|
||||
setError(
|
||||
e instanceof Error ? e.message : 'Não foi possível comparar.',
|
||||
@@ -111,9 +149,11 @@ export default function ContextEditor({
|
||||
`/investimentos/${view.case.id}/contexto`,
|
||||
);
|
||||
setView(next);
|
||||
setPreparation(next.private_investment);
|
||||
navigation.refresh();
|
||||
setChanges({});
|
||||
setDeclarations({});
|
||||
setDocumentLinks({});
|
||||
setReplace([]);
|
||||
setPlan(null);
|
||||
setOperation(createOperationKey());
|
||||
@@ -132,7 +172,10 @@ export default function ContextEditor({
|
||||
}
|
||||
return (
|
||||
<Frame title="Contexto do investimento">
|
||||
<InvestmentContextAssessment contextId={view.case.id} raw={view.prefill.simulator?.context_applicability_snapshot} />
|
||||
<InvestmentContextAssessment
|
||||
contextId={view.case.id}
|
||||
raw={view.prefill.simulator?.context_applicability_snapshot}
|
||||
/>
|
||||
<ContextDocuments source={view.context.selection.opportunity} />
|
||||
<div className="space-y-5 [overflow-wrap:anywhere]">
|
||||
<Link
|
||||
@@ -337,17 +380,107 @@ export default function ContextEditor({
|
||||
Estes campos pertencem ao processo. Alterá-los não muda
|
||||
o cadastro da oportunidade nem o perfil da organização.
|
||||
</p>
|
||||
<p>Origem do investidor, residência fiscal e constituição da entidade são declarações distintas. A localização não confirma benefícios. A prévia identifica o impacto antes de guardar.</p>
|
||||
<p>
|
||||
Origem do investidor, residência fiscal e constituição
|
||||
da entidade são declarações distintas. A localização não
|
||||
confirma benefícios. A prévia identifica o impacto antes
|
||||
de guardar.
|
||||
</p>
|
||||
{[
|
||||
['currency','Moeda do projecto',null],
|
||||
['expected_start_date','Data prevista de início',null],
|
||||
['investor_origin','Origem do investidor',[['national','Nacional'],['foreign','Estrangeiro'],['mixed','Misto']]],
|
||||
['person_type','Tipo de entidade',[['singular','Pessoa singular'],['collective','Pessoa colectiva'],['consortium','Consórcio']]],
|
||||
['entity_stage','Situação da entidade',[['existing','Existente'],['being_formed','Em constituição']]],
|
||||
['tax_residency_country','País de residência fiscal declarado (ISO de duas letras)',null],
|
||||
].map(([key,label,choices]) => <label className="grid gap-1" key={String(key)}>{String(label)}{Array.isArray(choices)
|
||||
? <select className={input} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}}><option value="">Por indicar</option>{choices.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select>
|
||||
: <input className={input} type={key==='expected_start_date'?'date':'text'} maxLength={key==='currency'?3:key==='tax_residency_country'?2:10} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}} />}</label>)}
|
||||
['currency', 'Moeda do projecto', null],
|
||||
[
|
||||
'expected_start_date',
|
||||
'Data prevista de início',
|
||||
null,
|
||||
],
|
||||
[
|
||||
'investor_origin',
|
||||
'Origem do investidor',
|
||||
[
|
||||
['national', 'Nacional'],
|
||||
['foreign', 'Estrangeiro'],
|
||||
['mixed', 'Misto'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'person_type',
|
||||
'Tipo de entidade',
|
||||
[
|
||||
['singular', 'Pessoa singular'],
|
||||
['collective', 'Pessoa colectiva'],
|
||||
['consortium', 'Consórcio'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'entity_stage',
|
||||
'Situação da entidade',
|
||||
[
|
||||
['existing', 'Existente'],
|
||||
['being_formed', 'Em constituição'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tax_residency_country',
|
||||
'País de residência fiscal declarado (ISO de duas letras)',
|
||||
null,
|
||||
],
|
||||
].map(([key, label, choices]) => (
|
||||
<label className="grid gap-1" key={String(key)}>
|
||||
{String(label)}
|
||||
{Array.isArray(choices) ? (
|
||||
<select
|
||||
className={input}
|
||||
value={
|
||||
declarations[String(key)] ??
|
||||
view.context.fields[String(key)] ??
|
||||
''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setDeclarations({
|
||||
...declarations,
|
||||
[String(key)]: e.target.value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{choices.map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
className={input}
|
||||
type={
|
||||
key === 'expected_start_date'
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
maxLength={
|
||||
key === 'currency'
|
||||
? 3
|
||||
: key === 'tax_residency_country'
|
||||
? 2
|
||||
: 10
|
||||
}
|
||||
value={
|
||||
declarations[String(key)] ??
|
||||
view.context.fields[String(key)] ??
|
||||
''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setDeclarations({
|
||||
...declarations,
|
||||
[String(key)]: e.target.value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
{['project_title', 'project_summary'].map((k) => (
|
||||
<label className="grid gap-1" key={k}>
|
||||
{contextFieldNames[k]}
|
||||
@@ -379,18 +512,7 @@ export default function ContextEditor({
|
||||
declarações do utilizador. Autorize somente os
|
||||
campos que pretende substituir pelo cadastro.
|
||||
</p>
|
||||
<p>Origem do investidor, residência fiscal e constituição da entidade são declarações distintas. A localização não confirma benefícios. A prévia identifica o impacto antes de guardar.</p>
|
||||
{[
|
||||
['currency','Moeda do projecto',null],
|
||||
['expected_start_date','Data prevista de início',null],
|
||||
['investor_origin','Origem do investidor',[['national','Nacional'],['foreign','Estrangeiro'],['mixed','Misto']]],
|
||||
['person_type','Tipo de entidade',[['singular','Pessoa singular'],['collective','Pessoa colectiva'],['consortium','Consórcio']]],
|
||||
['entity_stage','Situação da entidade',[['existing','Existente'],['being_formed','Em constituição']]],
|
||||
['tax_residency_country','País de residência fiscal declarado (ISO de duas letras)',null],
|
||||
].map(([key,label,choices]) => <label className="grid gap-1" key={String(key)}>{String(label)}{Array.isArray(choices)
|
||||
? <select className={input} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}}><option value="">Por indicar</option>{choices.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select>
|
||||
: <input className={input} type={key==='expected_start_date'?'date':'text'} maxLength={key==='currency'?3:key==='tax_residency_country'?2:10} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}} />}</label>)}
|
||||
{['project_title', 'project_summary'].map((k) => (
|
||||
{['project_title', 'project_summary'].map((k) => (
|
||||
<label className="block" key={k}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -411,6 +533,30 @@ export default function ContextEditor({
|
||||
</label>
|
||||
))}
|
||||
</details>
|
||||
{view.private_investment && (
|
||||
<PreparationEditor
|
||||
data={preparation ?? view.private_investment}
|
||||
fields={view.context.fields}
|
||||
changes={declarations}
|
||||
documents={view.preparation_documents ?? []}
|
||||
links={documentLinks}
|
||||
disabled={busy || !view.can_write}
|
||||
onChange={(key, value) => {
|
||||
setDeclarations({
|
||||
...declarations,
|
||||
[key]: value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
onLink={(key, value) => {
|
||||
setDocumentLinks({
|
||||
...documentLinks,
|
||||
[key]: value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className={button}
|
||||
type="button"
|
||||
@@ -467,10 +613,15 @@ export default function ContextEditor({
|
||||
x.field
|
||||
] ?? x.field}
|
||||
:{' '}
|
||||
{x.before ??
|
||||
'não informado'}{' '}
|
||||
{displayValue(
|
||||
x.field,
|
||||
x.before,
|
||||
)}{' '}
|
||||
→{' '}
|
||||
{x.after ?? 'não informado'}
|
||||
{displayValue(
|
||||
x.field,
|
||||
x.after,
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
{Object.values(plan.selection).map(
|
||||
@@ -530,6 +681,9 @@ export default function ContextEditor({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{plan.private_investment && (
|
||||
<PreparationImpact {...plan.private_investment} />
|
||||
)}
|
||||
<p>{plan.notice}</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
PreparationSummary,
|
||||
type Preparation,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import ContextDocuments from '@/components/context-documents';
|
||||
import type { DossierContext } from '@/lib/investor-context';
|
||||
import FintechInvestment, {
|
||||
@@ -27,6 +31,7 @@ type Step = {
|
||||
source_paragraph: number;
|
||||
};
|
||||
type Path = {
|
||||
private_investment?: Preparation | null;
|
||||
fintech: FintechView | null;
|
||||
investment_context: DossierContext | null;
|
||||
zone_contexts?: {
|
||||
@@ -180,6 +185,28 @@ function Stage({ step, path }: { step: Step; path: Path }) {
|
||||
independente.
|
||||
</label>
|
||||
)}
|
||||
{path.private_investment?.enabled && (
|
||||
<div className="space-y-2">
|
||||
{path.private_investment.requirements
|
||||
.filter((r) => r.stage === step.code)
|
||||
.map((r) => (
|
||||
<p className="text-sm" key={r.key}>
|
||||
<strong>{r.label}:</strong>{' '}
|
||||
{r.state === 'not_applicable'
|
||||
? 'não exigido neste contexto; histórico preservado'
|
||||
: r.state === 'associated'
|
||||
? 'documento associado; conteúdo por conferir'
|
||||
: r.state === 'missing'
|
||||
? 'documento por associar'
|
||||
: 'aplicabilidade por determinar'}
|
||||
. {r.reason}{' '}
|
||||
<a className="underline" href={r.source.url}>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step.reason && (
|
||||
<p className="text-sm">Fundamento: {step.reason}</p>
|
||||
)}
|
||||
@@ -415,6 +442,10 @@ export default function InvestmentPath(path: Path) {
|
||||
<ContextDocuments
|
||||
source={path.investment_context?.selection.opportunity}
|
||||
/>
|
||||
<PreparationSummary
|
||||
data={path.private_investment}
|
||||
caseId={path.case.id}
|
||||
/>
|
||||
{path.investment_context && (
|
||||
<section className="space-y-2 rounded border border-emerald-300 bg-emerald-50 p-4">
|
||||
<h2 className="font-semibold">
|
||||
|
||||
Reference in New Issue
Block a user