Files
Investidor-V2/resources/js/components/desk-internal-operations.tsx
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

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