541 lines
20 KiB
TypeScript
541 lines
20 KiB
TypeScript
import { createOperationKey } from '@/lib/operation-key';
|
|
import { useForm } from '@inertiajs/react';
|
|
import { card, input, button } from '@/pages/investment/shared';
|
|
|
|
export type DeskDocument = {
|
|
id: string;
|
|
document_id: string;
|
|
file_name: string;
|
|
status: string;
|
|
malware_scan_status: string;
|
|
category: string;
|
|
source_reference: string;
|
|
};
|
|
export function DocumentReview({
|
|
caseId,
|
|
version,
|
|
documents,
|
|
requestId,
|
|
}: {
|
|
caseId: string;
|
|
version: number;
|
|
documents: DeskDocument[];
|
|
requestId?: string;
|
|
}) {
|
|
const form = useForm({
|
|
version,
|
|
request_id: requestId ?? null,
|
|
document: documents[0]?.id ?? '',
|
|
outcome: 'internal_verified',
|
|
what_checked: '',
|
|
evidence: '',
|
|
supports: [] as string[],
|
|
});
|
|
return (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.transform((d) => ({ ...d, version }));
|
|
form.post(
|
|
`/balcao-virtual/dossiers/${caseId}/documentos/${form.data.document}/conferencia`,
|
|
);
|
|
}}
|
|
>
|
|
<h3 className="text-xl font-bold">
|
|
Conferência documental interna
|
|
</h3>
|
|
<p>
|
|
O registo identifica a pessoa, o ficheiro e hash, o alcance e a
|
|
evidência. Não equivale a aprovação, registo ou licença de uma
|
|
instituição.
|
|
</p>
|
|
<label htmlFor={`review-document-${caseId}`}>Documento</label>
|
|
<select
|
|
id={`review-document-${caseId}`}
|
|
className={input}
|
|
required
|
|
value={form.data.document}
|
|
onChange={(e) => form.setData('document', e.target.value)}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{documents.map((d) => (
|
|
<option key={d.id} value={d.id}>
|
|
{d.file_name} · {d.malware_scan_status}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<label>
|
|
Resultado
|
|
<select
|
|
className={input}
|
|
value={form.data.outcome}
|
|
onChange={(e) => form.setData('outcome', e.target.value)}
|
|
>
|
|
<option value="internal_verified">
|
|
Conferido internamente no âmbito descrito
|
|
</option>
|
|
<option value="internal_issue">
|
|
Problema documental identificado
|
|
</option>
|
|
<option value="contradiction_identified">
|
|
Contradição identificada com evidência
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<fieldset className="space-y-2">
|
|
<legend>Pressupostos abrangidos por esta conferência</legend>
|
|
<p className="text-sm">
|
|
Seleccione apenas os âmbitos efectivamente verificados.
|
|
Associar um ficheiro não valida automaticamente um
|
|
pressuposto.
|
|
</p>
|
|
{Object.entries({
|
|
market_evidence: 'Mercado',
|
|
location_evidence: 'Localização',
|
|
resource_evidence: 'Recursos',
|
|
funding_evidence: 'Financiamento',
|
|
}).map(([key, label]) => (
|
|
<label key={key} className="flex gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.data.supports.includes(key)}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'supports',
|
|
e.target.checked
|
|
? [...form.data.supports, key]
|
|
: form.data.supports.filter(
|
|
(x) => x !== key,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
{label}
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
<label>
|
|
O que foi verificado
|
|
<textarea
|
|
className={input}
|
|
required
|
|
minLength={3}
|
|
value={form.data.what_checked}
|
|
onChange={(e) =>
|
|
form.setData('what_checked', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Evidência utilizada
|
|
<textarea
|
|
className={input}
|
|
required
|
|
minLength={3}
|
|
value={form.data.evidence}
|
|
onChange={(e) => form.setData('evidence', e.target.value)}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={form.processing || !documents.length}
|
|
>
|
|
Registar conferência com a minha autoria
|
|
</button>
|
|
{Object.entries(form.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export type DeskPayment = {
|
|
id: string;
|
|
reference: string;
|
|
amount: string;
|
|
currency: string;
|
|
source_reference: string;
|
|
document_id: string | null;
|
|
status: string;
|
|
verified_by: number | null;
|
|
reason: string | null;
|
|
version: number;
|
|
};
|
|
export function RequestOperations({
|
|
requestId,
|
|
owner,
|
|
reviewer,
|
|
documents,
|
|
payments,
|
|
}: {
|
|
requestId: string;
|
|
owner: boolean;
|
|
reviewer: boolean;
|
|
documents: DeskDocument[];
|
|
payments: DeskPayment[];
|
|
}) {
|
|
const payment = useForm({
|
|
reference: '',
|
|
amount: '',
|
|
currency: 'MZN',
|
|
source_reference: '',
|
|
document_id: '',
|
|
consent: false,
|
|
});
|
|
const review = useForm({
|
|
payment: payments[0]?.id ?? '',
|
|
version: payments[0]?.version ?? 1,
|
|
status: 'internal_verified',
|
|
reason: '',
|
|
evidence: '',
|
|
});
|
|
const communication = useForm({
|
|
channel: 'phone',
|
|
direction: 'received',
|
|
occurred_at: '',
|
|
summary: '',
|
|
evidence: '',
|
|
client_key: createOperationKey(),
|
|
});
|
|
return (
|
|
<section className="space-y-3">
|
|
<h2 className="text-2xl font-bold">
|
|
Pagamentos e comunicações — registos manuais
|
|
</h2>
|
|
<p>
|
|
Sem integração bancária, SMS ou WhatsApp nesta fase. Nenhum
|
|
registo abaixo confirma automaticamente pagamento ou entrega de
|
|
mensagem.
|
|
</p>
|
|
{payments.map((p) => (
|
|
<article key={p.id} className={card}>
|
|
<p>
|
|
{p.reference} · {p.amount} {p.currency} ·{' '}
|
|
{p.status === 'internal_verified'
|
|
? 'Comprovativo conferido internamente'
|
|
: p.status === 'internal_issue'
|
|
? 'Problema na conferência interna'
|
|
: 'Declarado — por conferir'}
|
|
</p>
|
|
<p>Fonte do valor: {p.source_reference}</p>
|
|
<p>
|
|
Responsável pela conferência:{' '}
|
|
{p.verified_by ?? 'Ainda sem conferência'} · {p.reason}
|
|
</p>
|
|
</article>
|
|
))}
|
|
{owner && (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
payment.post(
|
|
`/balcao-virtual/pedidos/${requestId}/pagamentos`,
|
|
);
|
|
}}
|
|
>
|
|
<h3 className="font-bold">
|
|
Registar referência e comprovativo
|
|
</h3>
|
|
<label>
|
|
Referência fornecida
|
|
<input
|
|
className={input}
|
|
required
|
|
value={payment.data.reference}
|
|
onChange={(e) =>
|
|
payment.setData('reference', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Valor declarado
|
|
<input
|
|
className={input}
|
|
required
|
|
type="number"
|
|
step="0.01"
|
|
min="0.01"
|
|
value={payment.data.amount}
|
|
onChange={(e) =>
|
|
payment.setData('amount', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Moeda
|
|
<select
|
|
className={input}
|
|
value={payment.data.currency}
|
|
onChange={(e) =>
|
|
payment.setData('currency', e.target.value)
|
|
}
|
|
>
|
|
{['MZN', 'USD', 'EUR'].map((c) => (
|
|
<option key={c}>{c}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Fonte do valor e finalidade
|
|
<textarea
|
|
className={input}
|
|
required
|
|
value={payment.data.source_reference}
|
|
onChange={(e) =>
|
|
payment.setData(
|
|
'source_reference',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Comprovativo associado ao mesmo dossier
|
|
<select
|
|
className={input}
|
|
value={payment.data.document_id}
|
|
onChange={(e) =>
|
|
payment.setData('document_id', e.target.value)
|
|
}
|
|
>
|
|
<option value="">
|
|
Pendente — registar referência agora
|
|
</option>
|
|
{documents.map((d) => (
|
|
<option key={d.id} value={d.document_id}>
|
|
{d.file_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={payment.data.consent}
|
|
onChange={(e) =>
|
|
payment.setData('consent', e.target.checked)
|
|
}
|
|
/>{' '}
|
|
Autorizo este registo interno para o pedido.
|
|
</label>
|
|
<button className={button} disabled={payment.processing}>
|
|
Guardar declaração
|
|
</button>
|
|
{Object.entries(payment.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
{reviewer && (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const current = payments.find(
|
|
(p) => p.id === review.data.payment,
|
|
);
|
|
review.transform((d) => ({
|
|
...d,
|
|
version: current?.version ?? 1,
|
|
}));
|
|
review.post(
|
|
`/balcao-virtual/pedidos/${requestId}/pagamentos/${review.data.payment}/conferencia`,
|
|
);
|
|
}}
|
|
>
|
|
<h3 className="font-bold">
|
|
Conferir comprovativo internamente
|
|
</h3>
|
|
<label>
|
|
Referência
|
|
<select
|
|
className={input}
|
|
required
|
|
value={review.data.payment}
|
|
onChange={(e) =>
|
|
review.setData('payment', e.target.value)
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{payments.map((p) => (
|
|
<option key={p.id} value={p.id}>
|
|
{p.reference}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Resultado
|
|
<select
|
|
className={input}
|
|
value={review.data.status}
|
|
onChange={(e) =>
|
|
review.setData('status', e.target.value)
|
|
}
|
|
>
|
|
<option value="internal_verified">
|
|
Comprovativo conferido internamente
|
|
</option>
|
|
<option value="internal_issue">
|
|
Problema identificado
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
O que foi conferido
|
|
<textarea
|
|
className={input}
|
|
required
|
|
value={review.data.reason}
|
|
onChange={(e) =>
|
|
review.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Evidência
|
|
<textarea
|
|
className={input}
|
|
required
|
|
value={review.data.evidence}
|
|
onChange={(e) =>
|
|
review.setData('evidence', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={review.processing || !payments.length}
|
|
>
|
|
Registar conferência
|
|
</button>
|
|
{Object.entries(review.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
{(owner || reviewer) && (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
communication.post(
|
|
`/balcao-virtual/pedidos/${requestId}/comunicacoes`,
|
|
{
|
|
onSuccess: () => {
|
|
communication.reset('summary', 'evidence');
|
|
communication.setData(
|
|
'client_key',
|
|
createOperationKey(),
|
|
);
|
|
},
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
<h3 className="font-bold">
|
|
Registar comunicação realizada fora do portal
|
|
</h3>
|
|
<label>
|
|
Canal
|
|
<select
|
|
className={input}
|
|
value={communication.data.channel}
|
|
onChange={(e) =>
|
|
communication.setData('channel', e.target.value)
|
|
}
|
|
>
|
|
{Object.entries({
|
|
phone: 'Telefone',
|
|
email: 'E-mail',
|
|
sms: 'SMS',
|
|
whatsapp: 'WhatsApp',
|
|
in_person: 'Presencial',
|
|
}).map(([k, v]) => (
|
|
<option key={k} value={k}>
|
|
{v}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Direcção
|
|
<select
|
|
className={input}
|
|
value={communication.data.direction}
|
|
onChange={(e) =>
|
|
communication.setData(
|
|
'direction',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="received">Recebida</option>
|
|
<option value="sent">Enviada manualmente</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Data real declarada
|
|
<input
|
|
className={input}
|
|
required
|
|
type="datetime-local"
|
|
value={communication.data.occurred_at}
|
|
onChange={(e) =>
|
|
communication.setData(
|
|
'occurred_at',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Resumo
|
|
<textarea
|
|
className={input}
|
|
required
|
|
value={communication.data.summary}
|
|
onChange={(e) =>
|
|
communication.setData('summary', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Evidência / referência
|
|
<textarea
|
|
className={input}
|
|
required
|
|
value={communication.data.evidence}
|
|
onChange={(e) =>
|
|
communication.setData(
|
|
'evidence',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={communication.processing}
|
|
>
|
|
Guardar registo manual — sem envio
|
|
</button>
|
|
{Object.entries(communication.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|