1216 lines
54 KiB
TypeScript
1216 lines
54 KiB
TypeScript
import { DossierPrefill } from '@/components/dossier-prefill';
|
|
import { createOperationKey } from '@/lib/operation-key';
|
|
import { deskFieldApplies } from '@/lib/desk-field-visibility';
|
|
import {
|
|
deskEmpty,
|
|
deskChoices,
|
|
deskScalar,
|
|
mergeDeskPrefill,
|
|
type DeskAnswer,
|
|
} from '@/lib/desk-field-values';
|
|
import DeskTypedChoice from '@/components/desk-typed-choice';
|
|
import DeskDraftRecovery from '@/components/desk-draft-recovery';
|
|
import DeskAttachmentField, {
|
|
type AttachmentInventory,
|
|
} from '@/components/desk-attachment-field';
|
|
import { useState } from 'react';
|
|
import type { Field } from '@/pages/desk/service-editor';
|
|
import { Link, useForm } from '@inertiajs/react';
|
|
import { Frame, card, input, button } from '@/pages/investment/shared';
|
|
import CountrySelect, { type Country } from '@/components/country-select';
|
|
import {
|
|
TerritoryFields,
|
|
type TerritoryOption,
|
|
} from '@/components/investment-territory';
|
|
import {
|
|
DocumentReview,
|
|
RequestOperations,
|
|
type DeskDocument,
|
|
type DeskPayment,
|
|
} from '@/components/desk-internal-operations';
|
|
|
|
const emptyDocuments: DeskDocument[] = [];
|
|
|
|
export default function RequestPage({
|
|
request: initialRequest,
|
|
case: dossier,
|
|
definition,
|
|
owner,
|
|
manager,
|
|
analyst,
|
|
fieldsEditable,
|
|
operationsEditable = !['closed', 'cancelled'].includes(
|
|
initialRequest.status,
|
|
),
|
|
states,
|
|
transitions,
|
|
findings,
|
|
messages,
|
|
analysts,
|
|
territoryPaths,
|
|
events,
|
|
documents,
|
|
reusableDocuments = emptyDocuments,
|
|
payments,
|
|
caseVersion,
|
|
countries,
|
|
toolPrefill,
|
|
boundContextRevision = 0,
|
|
serviceEffectiveFrom,
|
|
returnReasons = {},
|
|
}: {
|
|
returnReasons?: Record<string, string>;
|
|
serviceEffectiveFrom?: string | null;
|
|
boundContextRevision?: number;
|
|
request: {
|
|
id: string;
|
|
version: number;
|
|
status: string;
|
|
payload: Record<string, DeskAnswer>;
|
|
protocol: string | null;
|
|
editable_fields: string[];
|
|
analyst_id: number | null;
|
|
due_at: string | null;
|
|
};
|
|
case: { id: string; title: string };
|
|
definition: {
|
|
title: string;
|
|
fields: Record<string, Field>;
|
|
result: string;
|
|
};
|
|
owner: boolean;
|
|
manager: boolean;
|
|
analyst: boolean;
|
|
fieldsEditable: boolean;
|
|
operationsEditable?: boolean;
|
|
states: Record<string, string>;
|
|
transitions: string[];
|
|
findings: {
|
|
code: string;
|
|
explanation: string;
|
|
recommendation: string;
|
|
severity: string;
|
|
}[];
|
|
messages: {
|
|
id: string;
|
|
body: string;
|
|
name: string;
|
|
kind: string;
|
|
created_at: string;
|
|
}[];
|
|
analysts: { id: number; name: string }[];
|
|
territoryPaths: Record<string, TerritoryOption[]>;
|
|
documents: DeskDocument[];
|
|
reusableDocuments?: DeskDocument[];
|
|
payments: DeskPayment[];
|
|
caseVersion: number;
|
|
countries: Country[];
|
|
toolPrefill: {
|
|
description: string;
|
|
draft_id: string;
|
|
draft_version: number;
|
|
snapshot_sha256: string;
|
|
} | null;
|
|
events: {
|
|
id: string;
|
|
action: string;
|
|
name: string;
|
|
created_at: string;
|
|
payload: Record<string, unknown>;
|
|
}[];
|
|
}) {
|
|
const [recoveredRequest, setRecoveredRequest] = useState<
|
|
typeof initialRequest | null
|
|
>(null);
|
|
const request =
|
|
recoveredRequest?.id === initialRequest.id &&
|
|
recoveredRequest.version > initialRequest.version
|
|
? recoveredRequest
|
|
: initialRequest;
|
|
const [checkpointBusy, setCheckpointBusy] = useState(false);
|
|
const [checkpointBlocked, setCheckpointBlocked] = useState(false);
|
|
const [attachmentInventory, setAttachmentInventory] = useState<
|
|
| (AttachmentInventory & {
|
|
sourceDocuments: DeskDocument[];
|
|
sourceReusable: DeskDocument[];
|
|
})
|
|
| null
|
|
>(null);
|
|
const [attachmentBusy, setAttachmentBusy] = useState<
|
|
Record<string, boolean>
|
|
>({});
|
|
const filesBusy = Object.values(attachmentBusy).some(Boolean);
|
|
const inventoryNewer =
|
|
attachmentInventory && attachmentInventory.case_version > caseVersion;
|
|
const inventorySame =
|
|
attachmentInventory && attachmentInventory.case_version === caseVersion;
|
|
const currentInventory = {
|
|
case_version: Math.max(
|
|
attachmentInventory?.case_version ?? 0,
|
|
caseVersion,
|
|
),
|
|
documents:
|
|
attachmentInventory &&
|
|
(inventoryNewer ||
|
|
(inventorySame &&
|
|
attachmentInventory.sourceDocuments === documents))
|
|
? attachmentInventory.documents
|
|
: documents,
|
|
reusable_documents:
|
|
attachmentInventory &&
|
|
(inventoryNewer ||
|
|
(inventorySame &&
|
|
attachmentInventory.sourceReusable === reusableDocuments))
|
|
? (attachmentInventory.reusable_documents ?? emptyDocuments)
|
|
: reusableDocuments,
|
|
};
|
|
const form = useForm({
|
|
context_revision: 0,
|
|
context_reviewed: false,
|
|
version: request.version,
|
|
payload: request.payload,
|
|
consent: false,
|
|
reason: '',
|
|
submit: false,
|
|
});
|
|
const decision = useForm({
|
|
version: request.version,
|
|
status: transitions[0] ?? '',
|
|
reason: '',
|
|
editable_fields: [] as string[],
|
|
return_reason_code: 'unspecified',
|
|
});
|
|
const assignment = useForm({
|
|
version: request.version,
|
|
analyst_id: String(request.analyst_id ?? ''),
|
|
reason: '',
|
|
due_at: '',
|
|
});
|
|
const message = useForm({ body: '', client_key: createOperationKey() });
|
|
const canEdit =
|
|
owner &&
|
|
fieldsEditable &&
|
|
['draft', 'returned'].includes(request.status);
|
|
return (
|
|
<Frame title={definition.title}>
|
|
{serviceEffectiveFrom && (
|
|
<p>
|
|
Entrada em vigor do formulário deste pedido:{' '}
|
|
{serviceEffectiveFrom}. O pedido conserva a versão com que
|
|
foi criado.
|
|
</p>
|
|
)}
|
|
<Link
|
|
className="underline"
|
|
href={`/balcao-virtual/dossiers/${dossier.id}`}
|
|
>
|
|
Voltar ao dossier {dossier.title}
|
|
</Link>
|
|
<p>{definition.result}</p>
|
|
{canEdit && (
|
|
<DeskDraftRecovery
|
|
key={`${request.id}:${request.version}`}
|
|
id={request.id}
|
|
version={request.version}
|
|
payload={form.data.payload}
|
|
initial={request.payload}
|
|
reason={form.data.reason}
|
|
fields={Object.fromEntries(
|
|
Object.entries(definition.fields).map(
|
|
([key, field]) => [
|
|
key,
|
|
field.type === 'attachment'
|
|
? {
|
|
...field,
|
|
options: Object.fromEntries(
|
|
currentInventory.documents.map(
|
|
(document) => [
|
|
document.id,
|
|
document.file_name,
|
|
],
|
|
),
|
|
),
|
|
}
|
|
: field,
|
|
],
|
|
),
|
|
)}
|
|
paused={form.processing}
|
|
pendingWork={filesBusy}
|
|
onBusy={setCheckpointBusy}
|
|
onBlocked={setCheckpointBlocked}
|
|
onRecover={(payload, reason, current) => {
|
|
setRecoveredRequest({
|
|
...request,
|
|
...current,
|
|
payload: current.payload,
|
|
});
|
|
form.setData((d) => ({
|
|
...d,
|
|
payload,
|
|
reason,
|
|
version: current.version,
|
|
consent: false,
|
|
context_reviewed: false,
|
|
context_revision: 0,
|
|
}));
|
|
}}
|
|
/>
|
|
)}
|
|
{owner && (
|
|
<DossierPrefill
|
|
contextId={dossier.id}
|
|
consumer="desk"
|
|
persistInheritance={request.status !== 'returned'}
|
|
values={form.data.payload}
|
|
boundRevision={boundContextRevision}
|
|
readOnly={!canEdit}
|
|
allowedFields={[
|
|
...Object.keys(definition.fields),
|
|
'context_opportunity',
|
|
].filter(
|
|
(k) =>
|
|
!['consent', 'attachment'].includes(
|
|
definition.fields[k]?.type,
|
|
) &&
|
|
(request.status !== 'returned' ||
|
|
request.editable_fields.includes(k)),
|
|
)}
|
|
onReview={(revision, reviewed) =>
|
|
form.setData((d) => ({
|
|
...d,
|
|
context_revision: revision,
|
|
context_reviewed: reviewed,
|
|
}))
|
|
}
|
|
onApply={(patch, replace) =>
|
|
form.setData((d) => ({
|
|
...d,
|
|
payload: mergeDeskPrefill(
|
|
d.payload,
|
|
patch,
|
|
definition.fields,
|
|
replace,
|
|
),
|
|
}))
|
|
}
|
|
/>
|
|
)}
|
|
<section className={card}>
|
|
<h2 className="text-xl font-bold">
|
|
{request.protocol
|
|
? 'Pedido submetido pelo investidor'
|
|
: 'Rascunho — ainda não submetido'}
|
|
</h2>
|
|
<p>
|
|
{states[request.status]} · Protocolo:{' '}
|
|
{request.protocol ?? 'Ainda sem protocolo'}
|
|
</p>
|
|
<p>
|
|
Responsável real:{' '}
|
|
{request.analyst_id ?? 'A aguardar atribuição'} · Prazo
|
|
interno: {request.due_at ?? 'Não definido'}
|
|
</p>
|
|
{request.protocol && (
|
|
<a
|
|
className="underline"
|
|
href={`/balcao-virtual/pedidos/${request.id}/recibo?format=html`}
|
|
>
|
|
Consultar e imprimir recibo interno
|
|
</a>
|
|
)}
|
|
</section>
|
|
{canEdit && toolPrefill && (
|
|
<section
|
|
className={card + ' space-y-3'}
|
|
aria-label="Descrição guardada no projeto"
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
Reutilizar dados do projeto
|
|
</h2>
|
|
<p>
|
|
A descrição está vazia neste pedido. Pode reutilizar o
|
|
resumo já guardado na ferramenta deste dossier, versão{' '}
|
|
{toolPrefill.draft_version}.
|
|
</p>
|
|
<blockquote className="rounded border p-3 whitespace-pre-wrap">
|
|
{toolPrefill.description}
|
|
</blockquote>
|
|
<p>
|
|
Esta ação guarda o rascunho com os dados preenchidos
|
|
nesta página. Reveja-os antes de submeter.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
disabled={
|
|
form.processing ||
|
|
checkpointBusy ||
|
|
checkpointBlocked ||
|
|
filesBusy ||
|
|
!deskEmpty(form.data.payload.description)
|
|
}
|
|
onClick={() => {
|
|
form.transform((data) => ({
|
|
...data,
|
|
version: request.version,
|
|
submit: false,
|
|
payload: {
|
|
...data.payload,
|
|
description: toolPrefill.description,
|
|
},
|
|
tool_prefill: {
|
|
draft_id: toolPrefill.draft_id,
|
|
draft_version: toolPrefill.draft_version,
|
|
snapshot_sha256:
|
|
toolPrefill.snapshot_sha256,
|
|
},
|
|
}));
|
|
form.put(`/balcao-virtual/pedidos/${request.id}`, {
|
|
onSuccess: () =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
description: toolPrefill.description,
|
|
}),
|
|
});
|
|
}}
|
|
>
|
|
Reutilizar descrição guardada e guardar rascunho
|
|
</button>
|
|
</section>
|
|
)}
|
|
<section className={card + ' space-y-3'}>
|
|
<h2 className="text-xl font-bold">
|
|
Verificações automáticas de preparação
|
|
</h2>
|
|
{findings.map((f) => (
|
|
<p key={f.code}>
|
|
{f.explanation} {f.recommendation} (
|
|
{f.severity === 'error'
|
|
? 'Contradição nos valores declarados'
|
|
: 'Informação / evidência pendente'}
|
|
)
|
|
</p>
|
|
))}
|
|
<p>
|
|
A análise automática não é uma resposta do responsável nem
|
|
uma aprovação oficial.
|
|
</p>
|
|
</section>
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (
|
|
checkpointBusy ||
|
|
checkpointBlocked ||
|
|
filesBusy ||
|
|
!canEdit
|
|
)
|
|
return;
|
|
form.transform((d) => ({
|
|
...d,
|
|
version: request.version,
|
|
submit: false,
|
|
}));
|
|
form.put(`/balcao-virtual/pedidos/${request.id}`);
|
|
}}
|
|
>
|
|
<label className="block" htmlFor="request-country">
|
|
País do requerente (opcional)
|
|
</label>
|
|
<CountrySelect
|
|
id="request-country"
|
|
label="País do requerente (opcional)"
|
|
error={form.errors['payload.country']}
|
|
countries={countries}
|
|
value={deskScalar(form.data.payload.country)}
|
|
allowLegacyValue
|
|
onChange={(value) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
country: value,
|
|
})
|
|
}
|
|
disabled={
|
|
!canEdit ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes('country'))
|
|
}
|
|
/>
|
|
<label className="block" htmlFor="declared-origin-country">
|
|
País de origem do pedido (opcional)
|
|
</label>
|
|
<CountrySelect
|
|
id="declared-origin-country"
|
|
label="País de origem do pedido (opcional)"
|
|
error={form.errors['payload.declared_origin_country_code']}
|
|
countries={countries}
|
|
value={deskScalar(
|
|
form.data.payload.declared_origin_country_code,
|
|
)}
|
|
disabled={
|
|
!canEdit ||
|
|
form.processing ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
'declared_origin_country_code',
|
|
))
|
|
}
|
|
onChange={(value) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
declared_origin_country_code: value,
|
|
})
|
|
}
|
|
/>
|
|
<p>
|
|
Indique o país de onde apresenta este pedido, se desejar.
|
|
Este campo é distinto do país do requerente.
|
|
</p>
|
|
{Object.entries(definition.fields)
|
|
.filter(([key]) =>
|
|
deskFieldApplies(
|
|
key,
|
|
definition.fields,
|
|
form.data.payload,
|
|
),
|
|
)
|
|
.map(([key, f]) =>
|
|
f.type === 'territory' ? (
|
|
<fieldset
|
|
key={key}
|
|
className="space-y-2 rounded border p-3"
|
|
>
|
|
<legend>
|
|
{f.label}
|
|
{f.required ? ' *' : ''}
|
|
</legend>
|
|
<TerritoryFields
|
|
initial={territoryPaths[key]}
|
|
disabled={
|
|
!canEdit ||
|
|
form.processing ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
key,
|
|
))
|
|
}
|
|
onChange={(path) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
[key]: path.at(-1)?.id ?? '',
|
|
})
|
|
}
|
|
/>
|
|
{form.errors[`payload.${key}`] && (
|
|
<p role="alert">
|
|
{form.errors[`payload.${key}`]}
|
|
</p>
|
|
)}
|
|
</fieldset>
|
|
) : f.type === 'attachment' ? (
|
|
<div key={key}>
|
|
<DeskAttachmentField
|
|
caseId={dossier.id}
|
|
requestId={request.id}
|
|
caseVersion={currentInventory.case_version}
|
|
fieldKey={key}
|
|
field={f}
|
|
value={form.data.payload[key]}
|
|
documents={currentInventory.documents}
|
|
reusableDocuments={
|
|
currentInventory.reusable_documents
|
|
}
|
|
disabled={
|
|
!canEdit ||
|
|
form.processing ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
key,
|
|
))
|
|
}
|
|
uploading={filesBusy}
|
|
onBusy={(value) =>
|
|
setAttachmentBusy((state) => ({
|
|
...state,
|
|
[key]: value,
|
|
}))
|
|
}
|
|
onChange={(value) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
[key]: value,
|
|
})
|
|
}
|
|
onInventory={(result, select) => {
|
|
setAttachmentInventory({
|
|
...result,
|
|
reusable_documents:
|
|
result.reusable_documents ??
|
|
currentInventory.reusable_documents,
|
|
sourceDocuments: documents,
|
|
sourceReusable: reusableDocuments,
|
|
});
|
|
const linkId = result.link_id;
|
|
if (select && linkId)
|
|
form.setData((d) => {
|
|
const ids = deskChoices(
|
|
d.payload[key],
|
|
);
|
|
const uploaded =
|
|
result.documents.find(
|
|
(document) =>
|
|
document.id ===
|
|
linkId,
|
|
);
|
|
const alreadySelected =
|
|
ids.includes(linkId) ||
|
|
(!!uploaded &&
|
|
ids.some(
|
|
(id) =>
|
|
result.documents.find(
|
|
(
|
|
document,
|
|
) =>
|
|
document.id ===
|
|
id,
|
|
)
|
|
?.document_id ===
|
|
uploaded.document_id,
|
|
));
|
|
return {
|
|
...d,
|
|
payload: {
|
|
...d.payload,
|
|
[key]:
|
|
alreadySelected ||
|
|
ids.length >=
|
|
(f.max_files ??
|
|
1)
|
|
? ids
|
|
: [
|
|
...ids,
|
|
linkId,
|
|
],
|
|
},
|
|
};
|
|
});
|
|
}}
|
|
/>
|
|
{Object.entries(form.errors)
|
|
.filter(
|
|
([path]) =>
|
|
path === `payload.${key}` ||
|
|
path.startsWith(`payload.${key}.`),
|
|
)
|
|
.map(([path, error]) => (
|
|
<p role="alert" key={path}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
</div>
|
|
) : ['multiselect', 'consent'].includes(f.type) ? (
|
|
<div key={key}>
|
|
<DeskTypedChoice
|
|
fieldKey={key}
|
|
field={f}
|
|
value={form.data.payload[key]}
|
|
disabled={
|
|
!canEdit ||
|
|
form.processing ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
key,
|
|
))
|
|
}
|
|
onChange={(value) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
[key]: value,
|
|
})
|
|
}
|
|
/>
|
|
{Object.entries(form.errors)
|
|
.filter(
|
|
([path]) =>
|
|
path === `payload.${key}` ||
|
|
path.startsWith(`payload.${key}.`),
|
|
)
|
|
.map(([path, error]) => (
|
|
<p key={path} role="alert">
|
|
{error}
|
|
</p>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<label className="block" key={key}>
|
|
{f.label}
|
|
{f.required ? ' *' : ''}
|
|
{f.type === 'select' ? (
|
|
<select
|
|
className={input}
|
|
disabled={
|
|
!canEdit ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
key,
|
|
))
|
|
}
|
|
value={deskScalar(
|
|
form.data.payload[key],
|
|
)}
|
|
onChange={(e) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">Por preencher</option>
|
|
{Object.entries(f.options ?? {}).map(
|
|
([v, l]) => (
|
|
<option key={v} value={v}>
|
|
{l}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
) : f.type === 'textarea' ? (
|
|
<textarea
|
|
className={input}
|
|
disabled={
|
|
!canEdit ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
key,
|
|
))
|
|
}
|
|
value={deskScalar(
|
|
form.data.payload[key],
|
|
)}
|
|
onChange={(e) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
) : (
|
|
<input
|
|
type={
|
|
f.type === 'date'
|
|
? 'date'
|
|
: f.type === 'number'
|
|
? 'number'
|
|
: 'text'
|
|
}
|
|
step={
|
|
f.type === 'number'
|
|
? '0.01'
|
|
: undefined
|
|
}
|
|
className={input}
|
|
disabled={
|
|
!canEdit ||
|
|
(request.status === 'returned' &&
|
|
!request.editable_fields.includes(
|
|
key,
|
|
))
|
|
}
|
|
value={deskScalar(
|
|
form.data.payload[key],
|
|
)}
|
|
onChange={(e) =>
|
|
form.setData('payload', {
|
|
...form.data.payload,
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
</label>
|
|
),
|
|
)}
|
|
{canEdit && (
|
|
<>
|
|
{request.status === 'returned' && (
|
|
<label>
|
|
Justificação de correcção
|
|
<textarea
|
|
className={input}
|
|
value={form.data.reason}
|
|
onChange={(e) =>
|
|
form.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<button
|
|
className={button}
|
|
disabled={
|
|
form.processing ||
|
|
checkpointBusy ||
|
|
checkpointBlocked ||
|
|
filesBusy
|
|
}
|
|
>
|
|
Guardar rascunho
|
|
</button>
|
|
<label className="flex gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.data.consent}
|
|
onChange={(e) =>
|
|
form.setData('consent', e.target.checked)
|
|
}
|
|
/>
|
|
Revi o conteúdo e autorizo a submissão para análise
|
|
interna.
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
disabled={
|
|
form.processing ||
|
|
checkpointBusy ||
|
|
checkpointBlocked ||
|
|
filesBusy ||
|
|
!form.data.consent
|
|
}
|
|
onClick={() => {
|
|
if (
|
|
checkpointBusy ||
|
|
checkpointBlocked ||
|
|
filesBusy ||
|
|
!canEdit
|
|
)
|
|
return;
|
|
form.transform((d) => ({
|
|
...d,
|
|
version: request.version,
|
|
submit: true,
|
|
}));
|
|
form.put(
|
|
`/balcao-virtual/pedidos/${request.id}`,
|
|
);
|
|
}}
|
|
>
|
|
Submeter este pedido
|
|
</button>
|
|
</>
|
|
)}
|
|
{Object.entries(form.errors).map(([k, v]) => (
|
|
<p key={k} role="alert">
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
{operationsEditable &&
|
|
owner &&
|
|
['draft', 'returned'].includes(request.status) && (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (
|
|
decision.processing ||
|
|
form.processing ||
|
|
checkpointBusy ||
|
|
filesBusy
|
|
)
|
|
return;
|
|
decision.transform((data) => ({
|
|
...data,
|
|
version: request.version,
|
|
status: 'cancelled',
|
|
}));
|
|
decision.post(
|
|
`/balcao-virtual/pedidos/${request.id}/estado`,
|
|
{
|
|
onSuccess: (page) => {
|
|
const saved = page.props.request as
|
|
| typeof initialRequest
|
|
| undefined;
|
|
if (
|
|
saved?.id === request.id &&
|
|
saved.status === 'cancelled'
|
|
) {
|
|
form.setData(
|
|
'payload',
|
|
saved.payload,
|
|
);
|
|
form.setData(
|
|
'version',
|
|
saved.version,
|
|
);
|
|
form.setData('reason', '');
|
|
form.setData('consent', false);
|
|
setRecoveredRequest(null);
|
|
}
|
|
},
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
Cancelar este pedido
|
|
</h2>
|
|
<p>
|
|
O cancelamento conserva a última versão guardada, os
|
|
documentos e o histórico. O dossier continua
|
|
disponível; alterações por guardar não são
|
|
submetidas.
|
|
</p>
|
|
<label className="block">
|
|
Motivo do cancelamento
|
|
<textarea
|
|
className={input}
|
|
required
|
|
minLength={3}
|
|
maxLength={5000}
|
|
value={decision.data.reason}
|
|
onChange={(event) =>
|
|
decision.setData(
|
|
'reason',
|
|
event.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={
|
|
decision.processing ||
|
|
form.processing ||
|
|
checkpointBusy ||
|
|
filesBusy
|
|
}
|
|
>
|
|
Confirmar cancelamento deste pedido
|
|
</button>
|
|
{Object.entries(decision.errors).map(([key, error]) => (
|
|
<p role="alert" key={key}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
{operationsEditable &&
|
|
!owner &&
|
|
(manager || analyst) &&
|
|
transitions.length > 0 && (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
decision.transform((d) => ({
|
|
...d,
|
|
version: request.version,
|
|
}));
|
|
decision.post(
|
|
`/balcao-virtual/pedidos/${request.id}/estado`,
|
|
);
|
|
}}
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
Resposta / análise do responsável
|
|
</h2>
|
|
<label>
|
|
Estado
|
|
<select
|
|
className={input}
|
|
value={decision.data.status}
|
|
onChange={(e) =>
|
|
decision.setData('status', e.target.value)
|
|
}
|
|
>
|
|
{transitions
|
|
.filter(
|
|
(s) =>
|
|
manager ||
|
|
![
|
|
'accepted',
|
|
'rejected',
|
|
'closed',
|
|
].includes(s),
|
|
)
|
|
.map((s) => (
|
|
<option value={s} key={s}>
|
|
{states[s]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Fundamento da resposta
|
|
<textarea
|
|
className={input}
|
|
required
|
|
minLength={3}
|
|
value={decision.data.reason}
|
|
onChange={(e) =>
|
|
decision.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
{decision.data.status === 'returned' && (
|
|
<fieldset>
|
|
<legend>Devolução para correcção</legend>
|
|
<label className="block">
|
|
Categoria do motivo (opcional, para
|
|
indicadores)
|
|
<select
|
|
className={input}
|
|
value={decision.data.return_reason_code}
|
|
onChange={(e) =>
|
|
decision.setData(
|
|
'return_reason_code',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
{Object.entries(returnReasons).map(
|
|
([code, label]) => (
|
|
<option key={code} value={code}>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</label>
|
|
<p>Campos autorizados para correcção</p>
|
|
{Object.entries({
|
|
...definition.fields,
|
|
country: { label: 'País do requerente' },
|
|
declared_origin_country_code: {
|
|
label: 'Origem declarada da solicitação',
|
|
},
|
|
}).map(([k, f]) => (
|
|
<label className="block" key={k}>
|
|
<input
|
|
type="checkbox"
|
|
checked={decision.data.editable_fields.includes(
|
|
k,
|
|
)}
|
|
onChange={(e) =>
|
|
decision.setData(
|
|
'editable_fields',
|
|
e.target.checked
|
|
? [
|
|
...decision.data
|
|
.editable_fields,
|
|
k,
|
|
]
|
|
: decision.data.editable_fields.filter(
|
|
(v) => v !== k,
|
|
),
|
|
)
|
|
}
|
|
/>{' '}
|
|
{f.label}
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
)}
|
|
<button
|
|
className={button}
|
|
disabled={decision.processing}
|
|
>
|
|
Registar resposta fundamentada
|
|
</button>
|
|
{Object.entries(decision.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
{operationsEditable &&
|
|
manager &&
|
|
!['draft', 'closed', 'cancelled'].includes(request.status) && (
|
|
<form
|
|
className={card + ' space-y-3'}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
assignment.transform((d) => ({
|
|
...d,
|
|
version: request.version,
|
|
}));
|
|
assignment.post(
|
|
`/balcao-virtual/pedidos/${request.id}/responsavel`,
|
|
);
|
|
}}
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
Atribuir responsável existente
|
|
</h2>
|
|
<label>
|
|
Responsável
|
|
<select
|
|
className={input}
|
|
required
|
|
value={assignment.data.analyst_id}
|
|
onChange={(e) =>
|
|
assignment.setData(
|
|
'analyst_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{analysts.map((a) => (
|
|
<option key={a.id} value={a.id}>
|
|
{a.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Prazo interno
|
|
<input
|
|
className={input}
|
|
type="datetime-local"
|
|
value={assignment.data.due_at}
|
|
onChange={(e) =>
|
|
assignment.setData('due_at', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Motivo
|
|
<textarea
|
|
className={input}
|
|
required
|
|
value={assignment.data.reason}
|
|
onChange={(e) =>
|
|
assignment.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={assignment.processing}
|
|
>
|
|
Atribuir acompanhamento
|
|
</button>
|
|
{Object.entries(assignment.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
<section className={card + ' space-y-3'}>
|
|
<h2 className="text-xl font-bold">
|
|
Conversa do pedido existente
|
|
</h2>
|
|
{messages.map((m) => (
|
|
<article key={m.id} className="rounded border p-3">
|
|
<p>
|
|
{m.kind === 'responsible_response'
|
|
? 'Resposta de responsável'
|
|
: m.kind === 'investor_message'
|
|
? 'Mensagem do investidor'
|
|
: 'Mensagem histórica — tipo original não registado'}{' '}
|
|
— {m.name} · {m.created_at}
|
|
</p>
|
|
<p className="break-words whitespace-pre-wrap">
|
|
{m.body}
|
|
</p>
|
|
</article>
|
|
))}
|
|
{operationsEditable && (
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
message.post(
|
|
`/balcao-virtual/pedidos/${request.id}/mensagens`,
|
|
{
|
|
onSuccess: () => {
|
|
message.reset('body');
|
|
message.setData(
|
|
'client_key',
|
|
createOperationKey(),
|
|
);
|
|
},
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
<label>
|
|
Mensagem privada
|
|
<textarea
|
|
className={input}
|
|
disabled={message.processing}
|
|
value={message.data.body}
|
|
required
|
|
onChange={(e) =>
|
|
message.setData('body', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<button
|
|
className={button}
|
|
disabled={message.processing}
|
|
>
|
|
Enviar esta mensagem no pedido
|
|
</button>
|
|
{Object.entries(message.errors).map(([k, v]) => (
|
|
<p role="alert" key={k}>
|
|
{v}
|
|
</p>
|
|
))}
|
|
</form>
|
|
)}
|
|
</section>
|
|
{operationsEditable && !owner && (manager || analyst) && (
|
|
<DocumentReview
|
|
caseId={dossier.id}
|
|
version={caseVersion}
|
|
documents={documents}
|
|
requestId={request.id}
|
|
/>
|
|
)}
|
|
<RequestOperations
|
|
requestId={request.id}
|
|
owner={owner && operationsEditable}
|
|
reviewer={operationsEditable && !owner && (manager || analyst)}
|
|
documents={documents}
|
|
payments={payments}
|
|
/>
|
|
<section className={card}>
|
|
<h2 className="text-xl font-bold">Respostas e histórico</h2>
|
|
{events.map((e) => (
|
|
<article className="mt-3 border-t pt-3" key={e.id}>
|
|
<p>
|
|
{e.action} · {e.name} · {e.created_at}
|
|
</p>
|
|
{typeof e.payload.reason === 'string' && (
|
|
<p>{e.payload.reason}</p>
|
|
)}
|
|
{e.action === 'request_returned' && (
|
|
<p>
|
|
Categoria:{' '}
|
|
{typeof e.payload.return_reason_label ===
|
|
'string'
|
|
? e.payload.return_reason_label
|
|
: 'Sem classificação registada'}
|
|
</p>
|
|
)}
|
|
{[
|
|
'document_reviewed',
|
|
'payment_reviewed',
|
|
'manual_communication_recorded',
|
|
].includes(e.action) && (
|
|
<pre className="text-xs break-all whitespace-pre-wrap">
|
|
{JSON.stringify(e.payload, null, 2)}
|
|
</pre>
|
|
)}
|
|
</article>
|
|
))}
|
|
</section>
|
|
</Frame>
|
|
);
|
|
}
|