Files
Investidor-V2/resources/js/pages/land-assistance/case.tsx
T

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