470 lines
20 KiB
TypeScript
470 lines
20 KiB
TypeScript
import { InvestmentConsumerIdentity } from '@/components/investment-consumer-identity';
|
|
import { Link, router, useForm } from '@inertiajs/react';
|
|
import { useState } from 'react';
|
|
|
|
type Props = {
|
|
case: {
|
|
id: string;
|
|
reference: string;
|
|
version: number;
|
|
status: string;
|
|
payload: Record<string, any>;
|
|
updated_at: string;
|
|
};
|
|
documents: {
|
|
id: string;
|
|
category: string;
|
|
file_name: string;
|
|
status: 'active' | 'quarantined';
|
|
}[];
|
|
events: {
|
|
action: string;
|
|
version: number;
|
|
metadata: Record<string, unknown>;
|
|
created_at: string;
|
|
}[];
|
|
disclaimer: string;
|
|
management: boolean;
|
|
returnDossier?: { id: string; title: string; url: string } | null;
|
|
countryName: string;
|
|
declaredOrigin: { code: string | null; name: string };
|
|
territoryPaths: { id: string; name: string; location_type: string }[][];
|
|
};
|
|
const statusLabels: Record<string, string> = {
|
|
draft: 'Rascunho privado',
|
|
support_requested: 'Apoio solicitado ao InvestMoz',
|
|
internal_review: 'Análise interna',
|
|
waiting_applicant: 'A aguardar informação do requerente',
|
|
referral_prepared: 'Encaminhamento preparado (não submetido)',
|
|
closed: 'Encerrado no InvestMoz',
|
|
};
|
|
const actionLabels: Record<string, string> = {
|
|
draft_created: 'Rascunho criado',
|
|
draft_updated: 'Rascunho actualizado',
|
|
support_requested: 'Pedido de apoio registado',
|
|
status_changed: 'Estado interno actualizado',
|
|
attachment_added: 'Documento anexado',
|
|
applicant_update_recorded: 'Actualização comunicada pelo requerente',
|
|
};
|
|
const categoryLabels: Record<string, string> = {
|
|
sketch: 'Planta/esboço',
|
|
prior_right: 'Antecedente de direito',
|
|
exploration_plan: 'Plano de exploração',
|
|
official_receipt: 'Recibo/comunicação oficial',
|
|
consultation_minutes: 'Acta/minuta',
|
|
photo: 'Fotografia',
|
|
other: 'Outro',
|
|
};
|
|
|
|
export default function LandCase({
|
|
case: record,
|
|
documents,
|
|
events,
|
|
disclaimer,
|
|
management,
|
|
countryName,
|
|
declaredOrigin,
|
|
territoryPaths,
|
|
returnDossier,
|
|
}: Props) {
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [category, setCategory] = useState('other');
|
|
const [_status, _setStatus] = useState('internal_review');
|
|
const upload = useForm({
|
|
version: record.version,
|
|
category,
|
|
file: null as File | null,
|
|
});
|
|
const report = useForm({
|
|
version: record.version,
|
|
reported_status: '',
|
|
official_reference: '',
|
|
receiving_entity: '',
|
|
reported_at: '',
|
|
});
|
|
const change = useForm({
|
|
version: record.version,
|
|
status: 'internal_review',
|
|
evidence_reference: '',
|
|
});
|
|
const canEdit = ['draft', 'waiting_applicant'].includes(record.status);
|
|
const payload = record.payload ?? {};
|
|
const supportKinds: Record<string, string> = {
|
|
investment: 'Terra para investimento',
|
|
home: 'Habitação',
|
|
family_exploitation: 'Exploração familiar',
|
|
recognition: 'Reconhecimento/titulação',
|
|
regularization: 'Regularização cadastral',
|
|
delimitation: 'Delimitação/demarcação',
|
|
change_of_use: 'Alteração de uso/área',
|
|
transfer: 'Transmissão/herança',
|
|
existing_process: 'Processo existente',
|
|
conflict_guidance: 'Orientação sobre conflito/restrição',
|
|
other: 'Outro',
|
|
};
|
|
return (
|
|
<main className="mx-auto max-w-5xl space-y-6 p-6">
|
|
<InvestmentConsumerIdentity
|
|
raw={payload.context_opportunity_snapshot}
|
|
/>
|
|
<header className="space-y-2">
|
|
{returnDossier && (
|
|
<Link className="underline" href={returnDossier.url}>
|
|
Voltar ao dossier: {returnDossier.title}
|
|
</Link>
|
|
)}
|
|
<Link href="/investidor/terra-duat" className="underline">
|
|
Os meus pedidos Terra e DUAT
|
|
</Link>
|
|
<h1 className="text-3xl font-bold">
|
|
Acompanhamento {record.reference}
|
|
</h1>
|
|
<p>
|
|
Referência interna InvestMoz · versão {record.version} ·{' '}
|
|
{statusLabels[record.status] ?? record.status}
|
|
</p>
|
|
<p className="rounded border border-amber-400 bg-amber-50 p-3">
|
|
{disclaimer}
|
|
</p>
|
|
{record.status === 'referral_prepared' && (
|
|
<p className="rounded bg-amber-50 p-3">
|
|
O encaminhamento foi preparado internamente. Não há
|
|
confirmação de submissão nem de recepção por entidade
|
|
pública.
|
|
</p>
|
|
)}
|
|
</header>
|
|
{!management && canEdit && (
|
|
<Link
|
|
className="inline-block rounded border px-4 py-2"
|
|
href={`/investidor/terra-duat/${record.id}/editar`}
|
|
>
|
|
Continuar preenchimento
|
|
</Link>
|
|
)}
|
|
{!management && (
|
|
<Link
|
|
className="ml-3 inline-block rounded border px-4 py-2"
|
|
href={`/investidor/terra-duat/${record.id}/imprimir`}
|
|
>
|
|
Imprimir / guardar cópia preenchida como PDF
|
|
</Link>
|
|
)}
|
|
<section className="grid gap-4 rounded border p-4 sm:grid-cols-2">
|
|
<h2 className="text-xl font-semibold sm:col-span-2">
|
|
Resumo informado pelo requerente
|
|
</h2>
|
|
<p>
|
|
<strong>Tipo:</strong>{' '}
|
|
{supportKinds[payload.support_kind] ?? 'Por indicar'}
|
|
</p>
|
|
<p>
|
|
<strong>Requerente:</strong>{' '}
|
|
{payload.full_name ?? 'Por indicar'}
|
|
</p>
|
|
<p>
|
|
<strong>Categoria:</strong>{' '}
|
|
{payload.applicant_category ?? 'Por indicar'}
|
|
</p>
|
|
<p>
|
|
<strong>País do requerente:</strong> {countryName}
|
|
</p>
|
|
<p>
|
|
<strong>Origem declarada da solicitação:</strong>{' '}
|
|
{declaredOrigin.name}
|
|
</p>
|
|
<p>
|
|
<strong>Contacto preferido:</strong>{' '}
|
|
{payload.preferred_contact ?? 'Por indicar'}
|
|
</p>
|
|
<p className="sm:col-span-2">
|
|
<strong>Descrição:</strong>{' '}
|
|
{payload.request_description ?? 'Por indicar'}
|
|
</p>
|
|
<p className="sm:col-span-2">
|
|
<strong>Uso pretendido:</strong>{' '}
|
|
{payload.purpose ?? 'Por indicar'}
|
|
</p>
|
|
<p className="sm:col-span-2">
|
|
<strong>Localização declarada:</strong>{' '}
|
|
{(payload.parcels ?? [])
|
|
.map((parcel: any) => parcel.place_description)
|
|
.filter(Boolean)
|
|
.join('; ') || 'Por localizar/confirmar'}
|
|
</p>
|
|
<p className="text-sm sm:col-span-2">
|
|
{territoryPaths.map(
|
|
(path, index) =>
|
|
path.length > 0 && (
|
|
<span className="block" key={index}>
|
|
Parcela {index + 1}:{' '}
|
|
{path
|
|
.map((territory) => territory.name)
|
|
.join(' → ')}
|
|
</span>
|
|
),
|
|
)}
|
|
Dados e situação geográfica aqui exibidos são declarações do
|
|
requerente; não confirmam direitos ou disponibilidade
|
|
cadastral.
|
|
</p>
|
|
</section>
|
|
{!management && (
|
|
<section className="space-y-3 rounded border p-4">
|
|
<h2 className="text-xl font-semibold">
|
|
Comunicar actualização
|
|
</h2>
|
|
<p className="text-sm">
|
|
A informação fica identificada como comunicada por si e
|
|
não é confirmada automaticamente pelo InvestMoz.
|
|
</p>
|
|
<label className="block space-y-1">
|
|
<span>Actualização do estado *</span>
|
|
<textarea
|
|
className="w-full rounded border p-2"
|
|
value={report.data.reported_status}
|
|
onChange={(e) =>
|
|
report.setData(
|
|
'reported_status',
|
|
e.target.value,
|
|
)
|
|
}
|
|
maxLength={1000}
|
|
/>
|
|
</label>
|
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
<label>
|
|
Referência oficial (se tiver)
|
|
<input
|
|
className="mt-1 w-full rounded border p-2"
|
|
value={report.data.official_reference}
|
|
onChange={(e) =>
|
|
report.setData(
|
|
'official_reference',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Entidade indicada
|
|
<input
|
|
className="mt-1 w-full rounded border p-2"
|
|
value={report.data.receiving_entity}
|
|
onChange={(e) =>
|
|
report.setData(
|
|
'receiving_entity',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Data indicada
|
|
<input
|
|
type="date"
|
|
className="mt-1 w-full rounded border p-2"
|
|
value={report.data.reported_at}
|
|
onChange={(e) =>
|
|
report.setData(
|
|
'reported_at',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<button
|
|
className="rounded border px-4 py-2"
|
|
disabled={
|
|
!report.data.reported_status || report.processing
|
|
}
|
|
onClick={() =>
|
|
router.post(
|
|
`/investidor/terra-duat/${record.id}/actualizacoes`,
|
|
report.data,
|
|
{ preserveScroll: true },
|
|
)
|
|
}
|
|
>
|
|
Registar informação comunicada
|
|
</button>
|
|
</section>
|
|
)}
|
|
<section className="space-y-3 rounded border p-4">
|
|
<h2 className="text-xl font-semibold">Documentos anexados</h2>
|
|
{documents.map((document) => (
|
|
<p key={document.id}>
|
|
{document.status === 'active' ? (
|
|
<a
|
|
className="underline"
|
|
href={`/${management ? 'management' : 'investidor'}/terra-duat/${record.id}/documentos/${document.id}`}
|
|
>
|
|
{categoryLabels[document.category] ??
|
|
document.category}
|
|
: {document.file_name}
|
|
</a>
|
|
) : (
|
|
<span>
|
|
{categoryLabels[document.category] ??
|
|
document.category}
|
|
: {document.file_name} · em quarentena;
|
|
leitura/descarga bloqueada até verificação
|
|
</span>
|
|
)}
|
|
</p>
|
|
))}
|
|
{!documents.length && (
|
|
<p>
|
|
Nenhum documento anexado. Anexos são opcionais nesta
|
|
fase.
|
|
</p>
|
|
)}
|
|
{!management && record.status !== 'closed' && (
|
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
<label>
|
|
Categoria
|
|
<select
|
|
className="mt-1 w-full rounded border p-2"
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value)}
|
|
>
|
|
{Object.entries(categoryLabels).map(
|
|
([key, label]) => (
|
|
<option value={key} key={key}>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</label>
|
|
<label className="sm:col-span-2">
|
|
Ficheiro PDF/JPG/PNG (até 10 MB)
|
|
<input
|
|
className="mt-1 w-full rounded border p-2"
|
|
type="file"
|
|
accept=".pdf,.jpg,.jpeg,.png"
|
|
onChange={(e) =>
|
|
setFile(e.target.files?.[0] ?? null)
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
className="rounded border px-4 py-2"
|
|
disabled={!file || upload.processing}
|
|
onClick={() =>
|
|
router.post(
|
|
`/investidor/terra-duat/${record.id}/documentos`,
|
|
{ version: record.version, category, file },
|
|
{
|
|
forceFormData: true,
|
|
preserveScroll: true,
|
|
},
|
|
)
|
|
}
|
|
>
|
|
Anexar em espaço privado
|
|
</button>
|
|
</div>
|
|
)}
|
|
<p className="text-sm">
|
|
Os ficheiros são reservados; anexar não equivale a
|
|
verificação, validação cadastral ou parecer oficial. O
|
|
ambiente não tem antivírus configurado, por isso os uploads
|
|
ficam em quarentena e não podem ser descarregados até
|
|
verificação.
|
|
</p>
|
|
</section>
|
|
<section className="space-y-3 rounded border p-4">
|
|
<h2 className="text-xl font-semibold">
|
|
Histórico no InvestMoz
|
|
</h2>
|
|
{events.map((event, index) => (
|
|
<div
|
|
className="border-l-2 pl-3"
|
|
key={`${event.version}-${index}`}
|
|
>
|
|
<p className="font-medium">
|
|
{actionLabels[event.action] ?? event.action}
|
|
</p>
|
|
<p className="text-sm">
|
|
{new Date(event.created_at).toLocaleString('pt-MZ')}{' '}
|
|
· versão {event.version}
|
|
</p>
|
|
{event.action === 'applicant_update_recorded' && (
|
|
<p className="text-xs">
|
|
Actualização comunicada pelo requerente; não
|
|
verificada externamente.
|
|
</p>
|
|
)}
|
|
{event.action === 'status_changed' &&
|
|
event.metadata?.external_submission_confirmed ===
|
|
false && (
|
|
<p className="text-xs">
|
|
Transição de estado interno; submissão
|
|
oficial não confirmada.
|
|
</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</section>
|
|
{management && (
|
|
<section className="space-y-3 rounded border border-slate-400 p-4">
|
|
<h2 className="text-xl font-semibold">Gestão interna</h2>
|
|
<label className="block">
|
|
Estado interno
|
|
<select
|
|
className="mt-1 w-full rounded border p-2"
|
|
value={change.data.status}
|
|
onChange={(e) =>
|
|
change.setData('status', e.target.value)
|
|
}
|
|
>
|
|
<option value="internal_review">
|
|
Análise interna
|
|
</option>
|
|
<option value="waiting_applicant">
|
|
A aguardar requerente
|
|
</option>
|
|
<option value="referral_prepared">
|
|
Encaminhamento preparado (não enviado)
|
|
</option>
|
|
<option value="closed">
|
|
Encerrado no InvestMoz
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{change.data.status === 'referral_prepared' && (
|
|
<label className="block">
|
|
Referência interna da preparação
|
|
<input
|
|
className="mt-1 w-full rounded border p-2"
|
|
value={change.data.evidence_reference}
|
|
onChange={(e) =>
|
|
change.setData(
|
|
'evidence_reference',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<button
|
|
className="rounded bg-slate-800 px-4 py-2 text-white"
|
|
disabled={change.processing}
|
|
onClick={() =>
|
|
router.patch(
|
|
`/management/terra-duat/${record.id}/estado`,
|
|
{ ...change.data, version: record.version },
|
|
{ preserveScroll: true },
|
|
)
|
|
}
|
|
>
|
|
Actualizar estado interno
|
|
</button>
|
|
</section>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|