Files
Investidor-V2/resources/js/components/simulation-offer-workflow.tsx
T

440 lines
16 KiB
TypeScript

import { buildOfferPackage } from '@/lib/iva-offer-package';
import { useRef, useState } from 'react';
import {
emptyOfferRequest,
offerRequestLabels,
type OfferPreparation,
} from '@/lib/iva-offer-preparations';
import {
emptyOfferWorkflow,
offerWrittenRequest,
offerWorkflowSource,
offerWorkflowApplied,
recordOfferWorkflow,
offerAttachmentValid,
type OfferAttachment,
type OfferWorkflow,
} from '@/lib/iva-offer-workflow';
export function SimulationOfferWorkflow({
row,
draft,
onChange,
}: {
row: OfferPreparation;
draft: Record<string, unknown>;
onChange: (patch: Partial<OfferPreparation>) => void;
}) {
const stageLabels: Record<string, string> = {
request: 'Pedido em preparação',
presented: 'Pedido apresentado',
decision: 'Decisão recebida',
form: 'Formulário preparado',
};
const latest = useRef(row);
latest.current = row;
const [notice, setNotice] = useState('');
const [error, setError] = useState(''),
[busy, setBusy] = useState(false),
[documents, setDocuments] = useState<OfferAttachment[]>([]),
[draftId, setDraftId] = useState('');
const w = row.workflow ?? emptyOfferWorkflow();
const edit = (patch: Partial<OfferWorkflow>) =>
onChange({
workflow: {
...(latest.current.workflow ?? emptyOfferWorkflow()),
...patch,
},
});
const action = async (fn: () => void | Promise<void>) => {
setBusy(true);
try {
await fn();
setError('');
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
};
const current = async () => {
const response = await fetch('/api/v1/simulation-drafts/current', {
credentials: 'same-origin',
cache: 'no-store',
headers: { Accept: 'application/json' },
});
if (!response.ok)
throw Error('Não foi possível localizar a gravação privada.');
const data = (await response.json()).data;
if (!data?.id)
throw Error(
'Guarde primeiro o rascunho no portal para lhe associar anexos privados.',
);
if (
data.economic_snapshot?.context_opportunity !==
draft.context_opportunity
)
throw Error(
'A gravação actual pertence a outro contexto;guarde este rascunho antes de anexar.',
);
setDraftId(data.id);
return data.id as string;
};
const list = async () => {
const id = await current(),
response = await fetch(
`/api/v1/simulation-drafts/${id}/offer-documents`,
{
credentials: 'same-origin',
headers: { Accept: 'application/json' },
},
);
if (response.status === 401 || response.status === 403)
throw Error(
'Autentique-se para anexar ou consultar documentos privados;os dados preparados mantêm-se.',
);
if (!response.ok)
throw Error('Não foi possível recuperar anexos privados.');
setDocuments((await response.json()).data);
};
const attach = async (
file: File,
field: 'requestAttachment' | 'decisionAttachment' | 'formAttachment',
) => {
const id = await current(),
data = new FormData();
data.append('file', file);
const csrf = document.querySelector<HTMLMetaElement>(
'meta[name="csrf-token"]',
)?.content;
const xsrf = document.cookie
.split('; ')
.find((c) => c.startsWith('XSRF-TOKEN='))
?.slice(11);
const response = await fetch(
`/api/v1/simulation-drafts/${id}/offer-documents`,
{
method: 'POST',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
...(xsrf
? { 'X-XSRF-TOKEN': decodeURIComponent(xsrf) }
: {}),
},
body: data,
},
);
if (!response.ok)
throw Error(
response.status === 401
? 'Autentique-se para guardar anexos privados.'
: 'Anexo não confirmado;rascunho preservado. ' +
(await response.text()).slice(0, 250),
);
const doc = (await response.json()).data;
if (!offerAttachmentValid(doc))
throw Error('Confirmação do anexo inválida.');
edit({ [field]: doc });
setDocuments((previous) => [
...previous.filter((d) => d.id !== doc.id),
doc,
]);
};
const download = (blob: Blob, name: string) => {
const url = URL.createObjectURL(blob),
a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
const packageForm = async () => {
const result = await buildOfferPackage(latest.current, async (url) => {
const response = await fetch(url, { credentials: 'same-origin' });
if (!response.ok)
throw Error(
'Anexo conservado;aguarde análise antimalware concluída antes de descarregar o conjunto.',
);
return response.arrayBuffer();
});
download(
new Blob([Uint8Array.from(result.bytes).buffer], {
type: 'application/zip',
}),
`${row.branch}-pedido-decisao-formulario.zip`,
);
onChange({ workflow: result.row.workflow });
setNotice(
'Conjunto preparado com pedido,elementos do formulário e despacho original conferido porSHA256;descarga iniciada.Submissão à autoridade permanece externa.',
);
};
const text = (
field:
| 'recipient'
| 'writtenRequest'
| 'decisionIssuer'
| 'formName'
| 'formSource'
| 'formContent'
| 'submissionReference',
label: string,
) => (
<label className="block" key={field}>
{label}
<textarea
className="block w-full rounded border p-2"
value={w[field]}
onChange={(e) => edit({ [field]: e.target.value })}
/>
</label>
);
const attachment = (
field: 'requestAttachment' | 'decisionAttachment' | 'formAttachment',
label: string,
) => (
<fieldset>
<legend>{label}</legend>
<label>
Reutilizar anexo privado
<select
value={w[field]?.id ?? ''}
onChange={(e) =>
edit({
[field]: documents.find(
(d) => d.id === e.target.value,
),
})
}
>
<option value="">Escolher documento existente</option>
{documents.map((d) => (
<option key={d.id} value={d.id}>
{d.name} — {d.scan}
</option>
))}
</select>
</label>
<label>
Juntar ficheiro privado
<input
type="file"
accept="application/pdf,text/plain,image/png,image/jpeg"
disabled={busy}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) void action(() => attach(file, field));
e.target.value = '';
}}
/>
</label>
{w[field] && (
<p>
{w[field]!.name};SHA256:{w[field]!.sha256};estado da análise
na associação:{w[field]!.scan}. Original privado
conservado;nenhuma publicação automática.
</p>
)}
</fieldset>
);
return (
<fieldset className="rounded border p-3">
<legend>Pedido → decisão → formulário com despacho</legend>
<p>{offerWorkflowSource}</p>
<p>
A submissão ao Ministério das Finanças/AT ocorre fora do
InvesteMoz. Pode preparar e guardar informação incompleta.
Guardar anexos privados reutiliza a autenticação e a análise
antimalware existentes;PDF,texto,PNG/JPEG até10MB é o limite
técnico de transferência,não condição do benefício.
</p>
{error && <p role="alert">{error}</p>}
{notice && <p role="status">{notice}</p>}
{!row.request && (
<button
type="button"
onClick={() =>
onChange({ request: emptyOfferRequest(), workflow: w })
}
>
Iniciar acompanhamento do pedido
</button>
)}
{text('recipient', 'Destinatário do pedido escrito')}
{text(
'writtenRequest',
'Justificação e elementos adicionais do pedido',
)}
<button
type="button"
onClick={() =>
download(
new Blob([offerWrittenRequest(row, w)], {
type: 'text/plain;charset=utf-8',
}),
`${row.branch}-pedido.txt`,
)
}
>
Preparar pedido escrito com os dados existentes
</button>
<button
type="button"
disabled={busy}
onClick={() => void action(list)}
>
Recuperar anexos privados desta gravação
</button>
{draftId && <p>Anexos associados à gravação privada actual.</p>}
{attachment(
'requestAttachment',
'Pedido escrito e comprovativo de apresentação,quando disponíveis',
)}
<button
type="button"
disabled={busy}
onClick={() =>
void action(() =>
onChange({
workflow: recordOfferWorkflow(
latest.current,
'request',
).workflow,
}),
)
}
>
Registar preparação do pedido
</button>
<button
type="button"
disabled={busy}
onClick={() =>
void action(() =>
onChange({
workflow: recordOfferWorkflow(
latest.current,
'presented',
).workflow,
}),
)
}
>
Registar pedido apresentado fora do portal
</button>
{text('decisionIssuer', 'Entidade emitente do despacho recebido')}
{attachment(
'decisionAttachment',
'Despacho recebido — original anexo',
)}
<label>
Esta linha está abrangida pelos bens,operação,período e
condições do despacho
<select
value={w.scopeConfirmed}
onChange={(e) =>
edit({
scopeConfirmed: e.target
.value as OfferWorkflow['scopeConfirmed'],
scopeKey: undefined,
})
}
>
<option value="">Por conferir</option>
<option value="yes">
Sim,apenas esta linha e âmbito indicado
</option>
<option value="no">Não</option>
</select>
</label>
<button
type="button"
disabled={busy}
onClick={() =>
void action(() =>
onChange({
workflow: recordOfferWorkflow(
latest.current,
'decision',
).workflow,
}),
)
}
>
Registar decisão recebida e âmbito desta linha
</button>
<p role="status">{offerWorkflowApplied(row).reason}</p>
{text('formName', 'Formulário aplicável:designação/versão')}
{text(
'formSource',
'Referência que demonstra a aplicabilidade deste formulário',
)}
{text(
'formContent',
'Elementos para o formulário,com despacho de isenção anexo',
)}
<button
type="button"
onClick={() =>
edit({
formContent:
w.formContent ||
`${offerWrittenRequest(row, w)}\nDespacho:${row.request?.decisionRef ?? ''};${row.request?.decisionOn ?? ''};emitente:${w.decisionIssuer}\nÂmbito:${row.request?.goods ?? ''};${row.request?.operationScope || row.request?.operation || ''};${row.request?.periodScope || `${row.request?.periodFrom ?? ''} a ${row.request?.periodTo ?? ''}`};${row.request?.conditions ?? ''}\nAnexo:${w.decisionAttachment?.name ?? ''};SHA256:${w.decisionAttachment?.sha256 ?? ''}`,
})
}
>
Reutilizar dados e despacho nos campos vazios do formulário
</button>
{attachment(
'formAttachment',
'Exemplar do formulário preenchido,quando disponível',
)}
<button
type="button"
disabled={busy}
onClick={() =>
void action(() =>
onChange({
workflow: recordOfferWorkflow(
latest.current,
'form',
).workflow,
}),
)
}
>
Preparar formulário com despacho associado
</button>
<button
type="button"
disabled={busy}
onClick={() => void action(packageForm)}
>
Descarregar conjunto do pedido,formulário e despacho
</button>
{text(
'submissionReference',
'Referência da apresentação externa do formulário,se já ocorreu',
)}
<p>
O ModeloI.1 não é seleccionado automaticamente para estes ramos.
O conteúdo preparado só substitui o preenchimento do exemplar
quando houver correspondência demonstrada;uma referência em
falta não apaga a decisão recebida.
</p>
{w.records.map((record, i) => (
<p key={i}>
{record.at} — {stageLabels[record.stage] ?? record.stage} —{' '}
{offerRequestLabels[record.request.state]};pedido:
{record.request.reference};despacho:
{record.request.decisionRef};bens:{record.request.goods}
;anexo:
{record.workflow.decisionAttachment?.name ||
'não registado'}
.
</p>
))}
</fieldset>
);
}