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

36 lines
9.5 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>;
}