Files
Investidor-V2/resources/js/pages/investor-path/show.tsx
T
2026-11-10 18:17:13 +02:00

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