1235 lines
54 KiB
TypeScript
1235 lines
54 KiB
TypeScript
import {
|
|
ProactiveSummary,
|
|
proactiveView,
|
|
} from '@/components/proactive-investment';
|
|
import {
|
|
PreparationSummary,
|
|
type Preparation,
|
|
} from '@/components/private-investment-preparation';
|
|
import ContextDocuments from '@/components/context-documents';
|
|
import { SelectedComplements } from '@/components/investment-complements';
|
|
import InvestmentRequirements from '@/components/investment-requirements';
|
|
import OpportunityChoices from '@/components/investment-opportunity-choices';
|
|
import type { DossierContext } from '@/lib/investor-context';
|
|
import FintechInvestment, {
|
|
type FintechView,
|
|
} from '@/components/fintech-investment';
|
|
import { createOperationKey } from '@/lib/operation-key';
|
|
import { Head, Link, router, useForm, usePage } from '@inertiajs/react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { revealPathStage } from '@/lib/investor-path-stage';
|
|
import {
|
|
pathStates,
|
|
recoverPathResult,
|
|
type PathResult,
|
|
} from '@/lib/investor-path';
|
|
|
|
type Step = {
|
|
code: string;
|
|
title: string;
|
|
phase: number;
|
|
state: string;
|
|
saved_state: string;
|
|
reason: string;
|
|
applicability: string;
|
|
preparation_allowed: boolean;
|
|
context_review_required?: boolean;
|
|
action_gaps: Record<string, string[]>;
|
|
detail: { scope?: string };
|
|
source_paragraph: number;
|
|
};
|
|
type Path = {
|
|
preparation_action?: {
|
|
id: string;
|
|
title: string;
|
|
kind: string;
|
|
case_id: string;
|
|
opportunity: string | null;
|
|
} | null;
|
|
context_opportunity: string | null;
|
|
opportunity_options: NonNullable<DossierContext['opportunity_choices']>;
|
|
private_investment?: Preparation | null;
|
|
fintech: FintechView | null;
|
|
investment_context: DossierContext | null;
|
|
zone_contexts?: {
|
|
zone_id: string;
|
|
consulted_public_version: number;
|
|
title: string;
|
|
href: string;
|
|
notice: string;
|
|
selected_at: string;
|
|
}[];
|
|
sector_contexts: { id: number; name: string; slug: string; href: string }[];
|
|
case: { id: string; title: string; version: number; closed: boolean };
|
|
phases: Record<string, string>;
|
|
steps: Step[];
|
|
definition_version: number;
|
|
facts: { land_needed: string; external_funding: string; viability: string };
|
|
fact_reasons: Record<string, string>;
|
|
canWrite: boolean;
|
|
canReview: boolean;
|
|
next: { code: string; title: string; href: string; action: string } | null;
|
|
tools: Record<string, string | null>;
|
|
results: PathResult[];
|
|
content_sources: {
|
|
resource_id: number;
|
|
title: string;
|
|
return_href: string;
|
|
source_url: string;
|
|
metadata_version: number;
|
|
}[];
|
|
documents: {
|
|
id: string;
|
|
document_id: string;
|
|
file_name: string;
|
|
status: string;
|
|
malware_scan_status: string;
|
|
}[];
|
|
requests: {
|
|
id: string;
|
|
status: string;
|
|
protocol: string | null;
|
|
context_state?: string;
|
|
context_revision?: number;
|
|
}[];
|
|
services: { id: string; title: string }[];
|
|
tasks: {
|
|
id: string;
|
|
title: string;
|
|
status: string;
|
|
version_no: number;
|
|
assignee_user_id: number;
|
|
due_at: string;
|
|
}[];
|
|
};
|
|
|
|
function Result({ result }: { result: PathResult }) {
|
|
try {
|
|
const values = recoverPathResult(result);
|
|
return (
|
|
<li className="space-y-1 rounded border p-3">
|
|
{result.context_opportunity && (
|
|
<p>
|
|
Oportunidade desta análise: {result.context_opportunity}
|
|
</p>
|
|
)}
|
|
{result.context_state === 'review_required' && (
|
|
<p
|
|
role="status"
|
|
className="rounded border border-amber-600 bg-amber-50 p-3 font-semibold"
|
|
>
|
|
Resultado histórico — desatualizado para o contexto
|
|
atual. Reveja as entradas e recalcule antes de o
|
|
utilizar. A evidência original foi preservada.
|
|
</p>
|
|
)}
|
|
<p>
|
|
Resultado financeiro ·{' '}
|
|
{result.currency ?? 'moeda por indicar'}
|
|
</p>
|
|
<p>
|
|
Ponto de equilíbrio:{' '}
|
|
{values.breakEven.value?.breakEvenUnits ??
|
|
values.breakEven.error}{' '}
|
|
unidades
|
|
</p>
|
|
<p>
|
|
Necessidade de financiamento:{' '}
|
|
{values.funding.value?.gap ?? values.funding.error}
|
|
</p>
|
|
<p className="text-sm">
|
|
Estimativa recalculada pelas fórmulas da ferramenta a partir
|
|
dos dados preenchidos. Não constitui decisão de
|
|
financiamento.
|
|
</p>
|
|
</li>
|
|
);
|
|
} catch (error) {
|
|
return (
|
|
<li role="alert">
|
|
{error instanceof Error
|
|
? error.message
|
|
: 'Resultado indisponível'}
|
|
</li>
|
|
);
|
|
}
|
|
}
|
|
|
|
function Stage({ step, path }: { step: Step; path: Path }) {
|
|
const form = useForm({
|
|
version: path.case.version,
|
|
context_opportunity: path.context_opportunity,
|
|
context_revision: path.investment_context?.revision ?? 0,
|
|
context_reviewed: false,
|
|
state: path.canWrite ? 'preparing' : 'reviewing',
|
|
action: path.canWrite ? 'prepare' : 'review',
|
|
reason: '',
|
|
document_id: '',
|
|
authority: '',
|
|
reference: '',
|
|
decision_date: '',
|
|
});
|
|
const ownerStates = [
|
|
'not_started',
|
|
'preparing',
|
|
'ready_to_submit',
|
|
'submitted',
|
|
'waiting_external',
|
|
'completed',
|
|
];
|
|
const reviewStates = [
|
|
'reviewing',
|
|
'correction_required',
|
|
'approved',
|
|
'rejected',
|
|
'expired',
|
|
];
|
|
const states = path.canWrite ? ownerStates : reviewStates;
|
|
const licensing = step.code === 'S08';
|
|
const Container = licensing ? 'section' : 'details';
|
|
const Heading = licensing ? 'h3' : 'summary';
|
|
const Reference = licensing ? 'div' : 'details';
|
|
const ReferenceHeading = licensing ? 'h4' : 'summary';
|
|
const Record = licensing ? 'section' : 'details';
|
|
const RecordHeading = licensing ? 'h4' : 'summary';
|
|
return (
|
|
<Container
|
|
id={step.code}
|
|
tabIndex={-1}
|
|
open={licensing ? undefined : path.next?.code === step.code}
|
|
className={`space-y-3 rounded-xl border p-4 ${licensing ? 'md:col-span-2' : ''}`}
|
|
>
|
|
<Heading
|
|
className={
|
|
licensing
|
|
? 'text-lg font-semibold'
|
|
: 'cursor-pointer font-semibold'
|
|
}
|
|
>
|
|
{step.title} · {pathStates[step.state]}
|
|
</Heading>
|
|
{!licensing && <p>{pathStates[step.state]}</p>}
|
|
{step.context_review_required && (
|
|
<label className="block rounded border border-amber-600 bg-amber-50 p-3">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.data.context_reviewed}
|
|
onChange={(e) =>
|
|
form.setData('context_reviewed', e.target.checked)
|
|
}
|
|
/>{' '}
|
|
Conferi os pressupostos desta etapa para o contexto atual.
|
|
Esta conferência não substitui evidência nem decisão
|
|
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>
|
|
)}
|
|
{step.applicability === 'undetermined' && (
|
|
<p className="text-sm">
|
|
Aplicabilidade por determinar. A preparação pode avançar.
|
|
</p>
|
|
)}
|
|
{step.state === 'dispensed' && (
|
|
<p className="text-sm">
|
|
Dispensa apenas de preparação deste ramo; não é dispensa
|
|
legal. O estado anterior e o histórico foram preservados.
|
|
</p>
|
|
)}
|
|
{Object.entries(step.action_gaps)
|
|
.filter(([, gaps]) => gaps.length)
|
|
.map(([action, gaps]) => (
|
|
<p key={action} className="text-sm">
|
|
{action === 'submit'
|
|
? 'Submissão'
|
|
: action === 'operate'
|
|
? 'Operação'
|
|
: 'Execução'}{' '}
|
|
aguarda{' '}
|
|
{gaps
|
|
.map(
|
|
(code) =>
|
|
path.steps.find(
|
|
(item) => item.code === code,
|
|
)?.title ?? code,
|
|
)
|
|
.join(', ')}
|
|
. Pode continuar a preparar.
|
|
</p>
|
|
))}
|
|
<Reference className="text-xs">
|
|
<ReferenceHeading
|
|
className={
|
|
licensing ? 'font-semibold' : 'cursor-pointer underline'
|
|
}
|
|
>
|
|
Fonte e referência desta etapa
|
|
</ReferenceHeading>
|
|
<p>
|
|
Referência interna {step.code}. Documento original,
|
|
parágrafo {step.source_paragraph}.{' '}
|
|
{step.detail.scope === 'internal_validation'
|
|
? 'Conferência interna; não equivale a decisão externa.'
|
|
: step.detail.scope === 'external_document_recorded'
|
|
? 'Acto externo registado a partir de documento.'
|
|
: 'Preparação declarada pelo promotor.'}
|
|
</p>
|
|
</Reference>
|
|
{step.preparation_allowed && (path.canWrite || path.canReview) && (
|
|
<Record
|
|
aria-label={
|
|
licensing ? 'Registar licenciamento' : undefined
|
|
}
|
|
>
|
|
<RecordHeading
|
|
className={
|
|
licensing
|
|
? 'font-semibold'
|
|
: 'cursor-pointer underline'
|
|
}
|
|
>
|
|
Registar avanço ou pendência
|
|
</RecordHeading>
|
|
<form
|
|
className="mt-3 grid gap-3"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
form.transform((data) => ({
|
|
...data,
|
|
version: path.case.version,
|
|
document_id: data.document_id || null,
|
|
decision_date: data.decision_date || null,
|
|
}));
|
|
form.post(
|
|
`/investimentos/${path.case.id}/etapas/${step.code}`,
|
|
{ preserveScroll: true },
|
|
);
|
|
}}
|
|
>
|
|
<label className="grid gap-1">
|
|
Estado
|
|
<select
|
|
className="rounded border p-2"
|
|
value={form.data.state}
|
|
onChange={(e) =>
|
|
form.setData('state', e.target.value)
|
|
}
|
|
>
|
|
{states.map((state) => (
|
|
<option key={state} value={state}>
|
|
{pathStates[state]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Acção
|
|
<select
|
|
className="rounded border p-2"
|
|
value={form.data.action}
|
|
onChange={(e) =>
|
|
form.setData('action', e.target.value)
|
|
}
|
|
>
|
|
{(path.canWrite
|
|
? [
|
|
['prepare', 'Preparar'],
|
|
['submit', 'Registar submissão'],
|
|
['execute', 'Executar'],
|
|
['operate', 'Operar'],
|
|
]
|
|
: [
|
|
['review', 'Conferência interna'],
|
|
[
|
|
'record_external',
|
|
'Registar acto externo',
|
|
],
|
|
]
|
|
).map(([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Fundamento
|
|
<textarea
|
|
required
|
|
minLength={3}
|
|
maxLength={3000}
|
|
className="rounded border p-2"
|
|
value={form.data.reason}
|
|
onChange={(e) =>
|
|
form.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Referência de submissão ou decisão
|
|
<input
|
|
className="rounded border p-2"
|
|
value={form.data.reference}
|
|
onChange={(e) =>
|
|
form.setData('reference', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
{path.canReview && (
|
|
<>
|
|
<label className="grid gap-1">
|
|
Documento de suporte
|
|
<select
|
|
className="rounded border p-2"
|
|
value={form.data.document_id}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'document_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Seleccionar</option>
|
|
{path.documents.map((doc) => (
|
|
<option
|
|
key={doc.id}
|
|
value={doc.document_id}
|
|
>
|
|
{doc.file_name} ·{' '}
|
|
{doc.malware_scan_status}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Entidade externa
|
|
<input
|
|
className="rounded border p-2"
|
|
value={form.data.authority}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'authority',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Data do acto
|
|
<input
|
|
type="date"
|
|
className="rounded border p-2"
|
|
value={form.data.decision_date}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'decision_date',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
</>
|
|
)}
|
|
{Object.values(form.errors).map((error, i) => (
|
|
<p key={i} role="alert">
|
|
{error}
|
|
</p>
|
|
))}
|
|
<button
|
|
disabled={form.processing}
|
|
className={
|
|
licensing
|
|
? 'rounded bg-emerald-800 px-4 py-3 font-semibold text-white disabled:opacity-50'
|
|
: 'rounded border p-2'
|
|
}
|
|
>
|
|
Guardar registo
|
|
</button>
|
|
</form>
|
|
</Record>
|
|
)}
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
export default function InvestmentPath(path: Path) {
|
|
const root = useRef<HTMLElement>(null);
|
|
const stageCodes = path.steps.map((step) => step.code).join(',');
|
|
useEffect(() => {
|
|
const reveal = () => {
|
|
if (root.current) {
|
|
revealPathStage(
|
|
root.current,
|
|
window.location.hash,
|
|
stageCodes.split(','),
|
|
);
|
|
if (
|
|
path.preparation_action &&
|
|
window.location.hash === '#tarefas'
|
|
) {
|
|
const target =
|
|
root.current.ownerDocument.getElementById('tarefas');
|
|
if (
|
|
target?.tagName === 'DETAILS' &&
|
|
root.current.contains(target)
|
|
) {
|
|
(target as HTMLDetailsElement).open = true;
|
|
target.scrollIntoView({ block: 'start' });
|
|
}
|
|
}
|
|
}
|
|
};
|
|
reveal();
|
|
window.addEventListener('hashchange', reveal);
|
|
return () => window.removeEventListener('hashchange', reveal);
|
|
}, [
|
|
path.case.id,
|
|
path.context_opportunity,
|
|
stageCodes,
|
|
path.next?.code,
|
|
path.preparation_action?.id,
|
|
]);
|
|
const { auth, flash } = usePage<{
|
|
auth: { user: { id: number } };
|
|
flash: { success?: string };
|
|
}>().props;
|
|
const facts = useForm({
|
|
version: path.case.version,
|
|
context_opportunity: path.context_opportunity,
|
|
...path.facts,
|
|
reason: '',
|
|
});
|
|
const task = useForm({
|
|
version: path.case.version,
|
|
code: 'S02',
|
|
title: path.preparation_action?.title ?? '',
|
|
preparation_action: path.preparation_action?.id ?? null,
|
|
opportunity: path.preparation_action?.opportunity ?? null,
|
|
due_at: '',
|
|
consent: false,
|
|
});
|
|
const [service, setService] = useState(path.services[0]?.id ?? '');
|
|
const [requestKey] = useState(() => createOperationKey());
|
|
const [taskReason, setTaskReason] = useState('');
|
|
const [error, setError] = useState('');
|
|
function support() {
|
|
router.post(
|
|
`/balcao-virtual/dossiers/${path.case.id}/pedidos`,
|
|
{
|
|
service_version_id: service,
|
|
client_key: requestKey,
|
|
opportunity: new URLSearchParams(window.location.search).get(
|
|
'opportunity',
|
|
),
|
|
},
|
|
{ onError: (errors) => setError(Object.values(errors).join('; ')) },
|
|
);
|
|
}
|
|
return (
|
|
<main
|
|
ref={root}
|
|
className="mx-auto max-w-6xl min-w-0 space-y-6 p-4 [overflow-wrap:anywhere] md:p-8 [&_input]:max-w-full [&_input]:min-w-0 [&_label]:min-w-0 [&_select]:max-w-full [&_select]:min-w-0"
|
|
>
|
|
<Head title={path.case.title} />
|
|
<Link className="underline" href="/investimentos">
|
|
Os meus investimentos
|
|
</Link>
|
|
<h1 className="text-2xl font-semibold">{path.case.title}</h1>
|
|
{path.investment_context?.selection.portfolio && (
|
|
<p className="rounded border p-4">
|
|
Origem documental:{' '}
|
|
<Link
|
|
className="underline"
|
|
href={path.investment_context.selection.portfolio.href}
|
|
>
|
|
{path.investment_context.selection.portfolio.title}
|
|
</Link>
|
|
. A localização do investimento é escolhida no contexto.
|
|
</p>
|
|
)}
|
|
{!!path.opportunity_options?.length && (
|
|
<label className="block rounded border p-4">
|
|
Oportunidade em tratamento
|
|
<select
|
|
className="ml-2 rounded border p-2"
|
|
value={path.context_opportunity ?? ''}
|
|
onChange={(event) =>
|
|
router.get(
|
|
`/investimentos/${path.case.id}`,
|
|
event.target.value
|
|
? { opportunity: event.target.value }
|
|
: {},
|
|
{ preserveState: false },
|
|
)
|
|
}
|
|
>
|
|
<option value="">Escolha a oportunidade</option>
|
|
{path.opportunity_options.map((choice) => (
|
|
<option
|
|
key={choice.opportunity.key}
|
|
value={choice.opportunity.key}
|
|
>
|
|
{choice.opportunity.title} —{' '}
|
|
{choice.territory?.title ??
|
|
'Localização por definir'}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{!path.context_opportunity &&
|
|
path.opportunity_options.length > 1 && (
|
|
<p>
|
|
Escolha para consultar as etapas, os requisitos
|
|
e o pedido de terra correspondentes. Os dados
|
|
comuns conservam-se.
|
|
</p>
|
|
)}
|
|
</label>
|
|
)}
|
|
<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">As suas escolhas</h2>
|
|
{Object.values(path.investment_context.selection).map(
|
|
(ref) => (
|
|
<p key={ref.key}>
|
|
<Link className="underline" href={ref.href}>
|
|
{ref.title}
|
|
</Link>
|
|
</p>
|
|
),
|
|
)}
|
|
<p>
|
|
Oportunidade, sector e território são referências de
|
|
preparação; não confirmam benefícios, disponibilidade ou
|
|
autorizações.
|
|
</p>
|
|
{path.canWrite && (
|
|
<Link
|
|
className="underline"
|
|
href={`/investimentos/${path.case.id}/contexto/editar`}
|
|
>
|
|
Comparar e alterar contexto
|
|
</Link>
|
|
)}
|
|
</section>
|
|
)}
|
|
{!!path.zone_contexts?.length && (
|
|
<section className="space-y-3 rounded border p-4">
|
|
<h2 className="font-semibold">
|
|
Parques anteriormente consultados — histórico preservado
|
|
</h2>
|
|
{path.zone_contexts.map((zone) => (
|
|
<article
|
|
key={`${zone.zone_id}:${zone.consulted_public_version}`}
|
|
>
|
|
<Link className="underline" href={zone.href}>
|
|
{zone.title}
|
|
</Link>
|
|
<p>{zone.selected_at}</p>
|
|
<p>{zone.notice}</p>
|
|
</article>
|
|
))}
|
|
</section>
|
|
)}
|
|
{!!path.sector_contexts?.length && (
|
|
<section className="rounded border p-4">
|
|
<h2 className="text-lg font-semibold">
|
|
Fontes setoriais anteriores — histórico preservado
|
|
</h2>
|
|
<ul>
|
|
{path.sector_contexts.map((s) => (
|
|
<li key={s.id}>
|
|
<Link className="underline" href={s.href}>
|
|
{s.name}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p>
|
|
Preparação privada nas mesmas cinco fases e 19 etapas; a
|
|
escolha do setor não publica uma oportunidade.
|
|
</p>
|
|
</section>
|
|
)}
|
|
{path.fintech && (
|
|
<FintechInvestment
|
|
data={path.fintech}
|
|
caseId={path.case.id}
|
|
version={path.case.version}
|
|
canWrite={path.canWrite}
|
|
documents={path.documents}
|
|
simulator={path.tools.simulator}
|
|
fromArticle={
|
|
path.content_sources.some(
|
|
(source) => source.return_href === '/tech-fintech',
|
|
) ||
|
|
path.sector_contexts.some((s) => s.slug === 'fintech')
|
|
}
|
|
/>
|
|
)}
|
|
{!!path.investment_context?.opportunity_choices?.length && (
|
|
<OpportunityChoices
|
|
contextId={path.case.id}
|
|
choices={path.investment_context.opportunity_choices}
|
|
/>
|
|
)}
|
|
{path.investment_context?.opportunity_choices?.map((choice) => (
|
|
<ProactiveSummary
|
|
key={choice.opportunity.key}
|
|
value={proactiveView(
|
|
choice.opportunity.analysis?.proactive,
|
|
)}
|
|
/>
|
|
))}
|
|
{!path.investment_context?.opportunity_choices?.length && (
|
|
<ProactiveSummary
|
|
value={proactiveView(
|
|
path.investment_context?.selection.opportunity?.analysis
|
|
?.proactive,
|
|
)}
|
|
/>
|
|
)}
|
|
<SelectedComplements
|
|
items={path.investment_context?.complements ?? []}
|
|
/>
|
|
<InvestmentRequirements
|
|
assessment={path.investment_context?.requirements_assessment}
|
|
contextId={path.case.id}
|
|
excludedSteps={path.steps
|
|
.filter((step) => !step.preparation_allowed)
|
|
.map((step) => step.code)}
|
|
/>
|
|
{!!path.content_sources?.length && (
|
|
<section
|
|
className="space-y-2 rounded-xl border p-4"
|
|
aria-label="Fontes da ideia"
|
|
>
|
|
<h2 className="font-semibold">
|
|
Conteúdos associados à ideia
|
|
</h2>
|
|
<ul>
|
|
{path.content_sources.map((source) => (
|
|
<li
|
|
key={`${source.resource_id}-${source.metadata_version}`}
|
|
>
|
|
<Link
|
|
className="underline"
|
|
href={`${source.return_href}?context=${path.case.id}`}
|
|
>
|
|
{source.title}
|
|
</Link>{' '}
|
|
·{' '}
|
|
<Link
|
|
className="underline"
|
|
href={`/documentos/${source.resource_id}`}
|
|
>
|
|
Exemplar e origem
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
{flash?.success && <output>{flash.success}</output>}
|
|
{error && <p role="alert">{error}</p>}
|
|
<section className="rounded-xl border p-4">
|
|
<h2 className="font-semibold">Próxima acção</h2>
|
|
{path.next ? (
|
|
<a className="underline" href={path.next.href}>
|
|
{path.next.action}: {path.next.title}
|
|
</a>
|
|
) : (
|
|
<p>Reveja as pendências e o acompanhamento.</p>
|
|
)}
|
|
<p className="text-sm">
|
|
Pode preparar etapas independentes enquanto aguarda
|
|
informação. As etapas abaixo adaptam-se às suas escolhas.
|
|
</p>
|
|
</section>
|
|
<nav
|
|
className="flex flex-wrap gap-4"
|
|
aria-label="Ferramentas do investimento"
|
|
>
|
|
{Object.entries(path.tools)
|
|
.filter(([key, url]) => url && key !== 'guidance')
|
|
.map(([key, url]) => (
|
|
<Link className="underline" key={key} href={url!}>
|
|
{
|
|
(
|
|
{
|
|
preparation: 'Preparação APIEX e SWOT',
|
|
simulator: 'Plano e simulador',
|
|
desk: 'Balcão e histórico',
|
|
library: 'Biblioteca',
|
|
opportunities: 'Oportunidades',
|
|
territory: 'Território e mapas',
|
|
land: 'Preparar Terra e DUAT',
|
|
partners: 'Parcerias',
|
|
} as Record<string, string>
|
|
)[key]
|
|
}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
{path.canWrite && (
|
|
<form
|
|
className="grid gap-3 rounded-xl border p-4 sm:grid-cols-2"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
facts.transform((data) => ({
|
|
...data,
|
|
version: path.case.version,
|
|
}));
|
|
facts.post(
|
|
`/investimentos/${path.case.id}/aplicabilidade`,
|
|
{ preserveScroll: true },
|
|
);
|
|
}}
|
|
>
|
|
<h2 className="font-semibold sm:col-span-2">
|
|
Decisões do promotor e aplicabilidade
|
|
</h2>
|
|
{(
|
|
[
|
|
['land_needed', 'Precisa de terra?'],
|
|
[
|
|
'external_funding',
|
|
'Precisa de financiamento externo?',
|
|
],
|
|
] as const
|
|
).map(([key, label]) => (
|
|
<label className="grid gap-1" key={key}>
|
|
{label}
|
|
<select
|
|
className="rounded border p-2"
|
|
value={facts.data[key]}
|
|
onChange={(e) =>
|
|
facts.setData(key, e.target.value)
|
|
}
|
|
>
|
|
<option value="unknown">Por determinar</option>
|
|
<option value="yes">Sim</option>
|
|
<option value="no">Não</option>
|
|
</select>
|
|
</label>
|
|
))}
|
|
<label className="grid gap-1">
|
|
Decisão sobre viabilidade
|
|
<select
|
|
className="rounded border p-2"
|
|
value={facts.data.viability}
|
|
onChange={(e) =>
|
|
facts.setData('viability', e.target.value)
|
|
}
|
|
>
|
|
<option value="unknown">Por determinar</option>
|
|
<option value="proceed">Prosseguir</option>
|
|
<option value="reformulate">Reformular</option>
|
|
<option value="archive">Arquivar a ideia</option>
|
|
</select>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Fundamento
|
|
<textarea
|
|
required
|
|
minLength={3}
|
|
className="rounded border p-2"
|
|
value={facts.data.reason}
|
|
onChange={(e) =>
|
|
facts.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
{Object.values(facts.errors).map((message, i) => (
|
|
<p key={i} role="alert">
|
|
{message}
|
|
</p>
|
|
))}
|
|
<button
|
|
disabled={facts.processing}
|
|
className="rounded border p-2"
|
|
>
|
|
Guardar decisões
|
|
</button>
|
|
</form>
|
|
)}
|
|
{path.steps.some((step) => !step.preparation_allowed) && (
|
|
<section
|
|
className="rounded-xl border p-4"
|
|
aria-label="Etapas sem aplicação"
|
|
>
|
|
<h2 className="font-semibold">
|
|
Não precisa de preparar neste contexto
|
|
</h2>
|
|
<ul>
|
|
{path.steps
|
|
.filter((step) => !step.preparation_allowed)
|
|
.map((step) => (
|
|
<li key={step.code}>
|
|
{step.title}:{' '}
|
|
{step.reason ||
|
|
'Conforme as escolhas guardadas.'}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="text-sm">
|
|
Pode rever estas escolhas acima. Os dados e o histórico
|
|
destas etapas ficam conservados.
|
|
</p>
|
|
</section>
|
|
)}
|
|
{Object.entries(path.phases)
|
|
.filter(([number]) =>
|
|
path.steps.some(
|
|
(step) =>
|
|
step.phase === Number(number) &&
|
|
step.preparation_allowed,
|
|
),
|
|
)
|
|
.map(([number, label]) => (
|
|
<details
|
|
open={path.steps.some(
|
|
(step) =>
|
|
step.phase === Number(number) &&
|
|
step.code === path.next?.code,
|
|
)}
|
|
className="space-y-3 rounded border p-4"
|
|
key={number}
|
|
>
|
|
<summary className="cursor-pointer text-lg font-semibold">
|
|
Fase {number} · {label}
|
|
</summary>
|
|
<div className="grid items-start gap-3 md:grid-cols-2">
|
|
{path.steps
|
|
.filter(
|
|
(step) =>
|
|
step.phase === Number(number) &&
|
|
step.preparation_allowed,
|
|
)
|
|
.map((step) => (
|
|
<Stage
|
|
key={step.code}
|
|
step={step}
|
|
path={path}
|
|
/>
|
|
))}
|
|
</div>
|
|
</details>
|
|
))}
|
|
<section className="space-y-3">
|
|
<h2 className="text-lg font-semibold">
|
|
Resultados devolvidos ao dossier
|
|
</h2>
|
|
<ul className="grid gap-3 md:grid-cols-2">
|
|
{path.results.map((result) => (
|
|
<Result key={result.id} result={result} />
|
|
))}
|
|
</ul>
|
|
{!path.results.length && (
|
|
<p>
|
|
Nenhum resultado financeiro disponível para esta conta
|
|
neste dossier.
|
|
</p>
|
|
)}
|
|
</section>
|
|
<section className="space-y-3 rounded-xl border p-4">
|
|
<h2 className="text-lg font-semibold">Apoio no Balcão</h2>
|
|
{path.canWrite && (
|
|
<>
|
|
<label className="grid gap-1">
|
|
Serviço
|
|
<select
|
|
className="rounded border p-2"
|
|
value={service}
|
|
onChange={(e) => setService(e.target.value)}
|
|
>
|
|
{path.services.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button
|
|
disabled={!service}
|
|
className="rounded border p-2"
|
|
onClick={support}
|
|
>
|
|
Preparar pedido com dados do dossier
|
|
</button>
|
|
<p className="text-sm">
|
|
Cria um rascunho; reveja os dados no Balcão antes de
|
|
submeter.
|
|
</p>
|
|
</>
|
|
)}
|
|
<ul>
|
|
{path.requests.map((item) => (
|
|
<li key={item.id}>
|
|
<Link
|
|
className="underline"
|
|
href={`/balcao-virtual/pedidos/${item.id}`}
|
|
>
|
|
{item.protocol ?? 'Rascunho'} · {item.status}
|
|
{item.context_state === 'historical_submission'
|
|
? ' · Submissão preservada num contexto anterior'
|
|
: item.context_state === 'review_required'
|
|
? ' · Rever os dados deste rascunho'
|
|
: ''}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
<section className="space-y-3 rounded-xl border p-4">
|
|
<h2 className="text-lg font-semibold">Documentos</h2>
|
|
<Link
|
|
className="block underline"
|
|
href={`/investimentos/${path.case.id}/guias/capitalizacao`}
|
|
>
|
|
Preparar capitalização e financiamento
|
|
</Link>
|
|
<Link
|
|
className="block underline"
|
|
href={`/investimentos/${path.case.id}/guias/bolsa`}
|
|
>
|
|
Preparar a admissão à Bolsa de Valores de Moçambique
|
|
</Link>
|
|
<Link
|
|
className="block underline"
|
|
href={`/investimentos/${path.case.id}/preparar-empresa`}
|
|
>
|
|
Preparar os dados para a constituição da empresa
|
|
</Link>
|
|
{path.tools.preparation && (
|
|
<Link className="underline" href={path.tools.preparation}>
|
|
Preparação APIEX e análise SWOT deste investimento
|
|
</Link>
|
|
)}
|
|
{path.tools.company && (
|
|
<Link className="underline" href={path.tools.company}>
|
|
Comparar formas jurídicas para este investimento
|
|
</Link>
|
|
)}
|
|
<ul>
|
|
{path.documents.map((doc) => (
|
|
<li key={doc.id}>
|
|
<Link
|
|
className="underline"
|
|
href={`/balcao-virtual/dossiers/${path.case.id}/documentos/${doc.id}`}
|
|
>
|
|
{doc.file_name}
|
|
</Link>{' '}
|
|
· {doc.status} · {doc.malware_scan_status}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<Link className="underline" href={path.tools.desk!}>
|
|
Anexar ou conferir no Balcão
|
|
</Link>
|
|
</section>
|
|
<details id="tarefas" className="space-y-3 rounded-xl border p-4">
|
|
<summary className="cursor-pointer text-lg font-semibold">
|
|
Acompanhamento interno
|
|
</summary>
|
|
{path.preparation_action && (
|
|
<p role="status">
|
|
Acção ligada à SWOT deste dossier:{' '}
|
|
{path.preparation_action.title}. Confira a etapa e a
|
|
data antes de assumir a tarefa.
|
|
</p>
|
|
)}
|
|
{path.canWrite && (
|
|
<form
|
|
className="grid min-w-0 gap-3 [&_input]:max-w-full [&_input]:min-w-0 [&_label]:min-w-0 [&_select]:max-w-full [&_select]:min-w-0"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
task.transform((data) => ({
|
|
...data,
|
|
version: path.case.version,
|
|
}));
|
|
task.post(
|
|
`/investimentos/${path.case.id}/tarefas`,
|
|
{ preserveScroll: true },
|
|
);
|
|
}}
|
|
>
|
|
<label className="grid gap-1">
|
|
Etapa
|
|
<select
|
|
className="rounded border p-2"
|
|
value={task.data.code}
|
|
onChange={(e) =>
|
|
task.setData('code', e.target.value)
|
|
}
|
|
>
|
|
{path.steps
|
|
.filter((step) => step.preparation_allowed)
|
|
.map((step) => (
|
|
<option
|
|
key={step.code}
|
|
value={step.code}
|
|
>
|
|
{step.code} · {step.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Tarefa
|
|
<input
|
|
required
|
|
className="rounded border p-2"
|
|
value={task.data.title}
|
|
onChange={(e) =>
|
|
task.setData('title', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="grid gap-1">
|
|
Prazo
|
|
<input
|
|
required
|
|
type="datetime-local"
|
|
className="w-full min-w-0 rounded border p-2"
|
|
value={task.data.due_at}
|
|
onChange={(e) =>
|
|
task.setData('due_at', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="flex gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={task.data.consent}
|
|
onChange={(e) =>
|
|
task.setData('consent', e.target.checked)
|
|
}
|
|
/>
|
|
Assumo esta tarefa no acompanhamento interno.
|
|
</label>
|
|
{Object.values(task.errors).map((message, i) => (
|
|
<p key={i} role="alert">
|
|
{message}
|
|
</p>
|
|
))}
|
|
<button
|
|
disabled={task.processing}
|
|
className="rounded border p-2"
|
|
>
|
|
Guardar tarefa
|
|
</button>
|
|
</form>
|
|
)}
|
|
<label className="grid gap-1">
|
|
Fundamento do resultado ou atribuição
|
|
<input
|
|
className="rounded border p-2"
|
|
value={taskReason}
|
|
onChange={(e) => setTaskReason(e.target.value)}
|
|
/>
|
|
</label>
|
|
<ul className="space-y-2">
|
|
{path.tasks.map((item) => (
|
|
<li className="rounded border p-3" key={item.id}>
|
|
{item.title} · {item.status} · prazo{' '}
|
|
{new Date(item.due_at).toLocaleString('pt-MZ')}
|
|
{(item.assignee_user_id === auth.user.id ||
|
|
path.canReview) && (
|
|
<div className="mt-2 flex flex-wrap gap-3">
|
|
{['in_progress', 'done'].map((status) => (
|
|
<button
|
|
key={status}
|
|
disabled={
|
|
taskReason.trim().length < 3
|
|
}
|
|
className="rounded border p-2"
|
|
onClick={() =>
|
|
router.post(
|
|
`/balcao-virtual/dossiers/${path.case.id}/tarefas/${item.id}`,
|
|
{
|
|
version:
|
|
item.version_no,
|
|
status,
|
|
reason: taskReason,
|
|
},
|
|
{
|
|
preserveScroll: true,
|
|
onError: (errors) =>
|
|
setError(
|
|
Object.values(
|
|
errors,
|
|
).join('; '),
|
|
),
|
|
},
|
|
)
|
|
}
|
|
>
|
|
{status === 'done'
|
|
? 'Registar conclusão'
|
|
: 'Iniciar tarefa'}
|
|
</button>
|
|
))}
|
|
{path.canReview && (
|
|
<button
|
|
disabled={
|
|
taskReason.trim().length < 3
|
|
}
|
|
className="rounded border p-2"
|
|
onClick={() =>
|
|
router.post(
|
|
`/balcao-virtual/dossiers/${path.case.id}/tarefas/${item.id}`,
|
|
{
|
|
version:
|
|
item.version_no,
|
|
status: item.status,
|
|
assignee_user_id:
|
|
auth.user.id,
|
|
reason: taskReason,
|
|
},
|
|
{ preserveScroll: true },
|
|
)
|
|
}
|
|
>
|
|
Assumir acompanhamento
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</details>
|
|
</main>
|
|
);
|
|
}
|