24 lines
8.5 KiB
React
24 lines
8.5 KiB
React
import React,{useEffect,useMemo,useState} from 'react';
|
|
import {WorkshopStockOperation} from './workshop-stock-operation.js';
|
|
import {minorAmount} from './ledger-report-view.js';
|
|
export const dueLabels={prepared:'Preparado — por rever',approved:'Aprovado',refused:'Recusado'};
|
|
export function DueFacts({data}){return <><p>{data.basis.document.number} · {data.basis.document.supplier_name}</p><p>Saldo actual {minorAmount(String(data.basis.position.balance_minor))} {data.basis.position.currency}. Vencimento original: {data.basis.position.due_on??'Desconhecido'}.</p><p>Condição administrativa revista: {data.reviewed_terms?.due_on??'Desconhecida'} · {data.days_overdue===null?'Atraso indeterminado':data.days_overdue+' dia(s) de atraso'} à referência {data.reference_on}.</p>{data.reviewed_terms&&<p>Evidência: {data.reviewed_terms.evidence}</p>}</>;}
|
|
export default function PayableDueWorkspace({row,api,context,onClose}){
|
|
const[reference,setReference]=useState(''),[query,setQuery]=useState(null),[data,setData]=useState(null),[form,setForm]=useState(null),[preview,setPreview]=useState(null),[error,setError]=useState(''),[notice,setNotice]=useState(''),[busy,setBusy]=useState(false),[receiptKey,setReceiptKey]=useState(''),[receipt,setReceipt]=useState(null);
|
|
const operation=useMemo(()=>new WorkshopStockOperation(api),[context.token]);const locked=busy||operation.uncertain;
|
|
useEffect(()=>{if(!query)return;let active=true;setBusy(true);setError('');api('/api/finance/payable-due?'+new URLSearchParams({obligation_id:row.obligation_id,...query})).then(v=>{if(active)setData(v);}).catch(e=>{if(active){setError(e.message);setData(null);}}).finally(()=>{if(active)setBusy(false);});return()=>{active=false;};},[context.token,row.obligation_id,query]);
|
|
function start(action){setError('');setReceipt(null);setPreview(null);setForm({action,obligation_id:row.obligation_id,version:data.basis.version,reason:'',data:action==='prepare'?{due_on:'',evidence:''}:{decision:''},unknown:false});}
|
|
async function previewForm(e){e.preventDefault();setBusy(true);setError('');try{const{unknown,...input}=form;if(input.action==='prepare')input.data={...input.data,due_on:unknown?null:input.data.due_on};const key=crypto.randomUUID();const out=await api('/api/finance/payable-due/preview','POST',input,undefined,key);setPreview({...out,key});}catch(e){setError(e.message);}finally{setBusy(false);}}
|
|
async function confirm(retry=false){setBusy(true);setError('');try{const out=retry?await operation.retry():await operation.send('/api/finance/payable-due/confirm',{preview:preview.preview},preview.key);setReceiptKey(preview.key);setNotice('Versão '+out.version+': '+dueLabels[out.state]+(out.replayed?' · recibo recuperado':''));setForm(null);setPreview(null);setQuery(q=>({...q,page:1}));}catch(e){setError(e.message);if(!operation.uncertain)setPreview(null);}finally{setBusy(false);}}
|
|
async function recoverReceipt(){setBusy(true);setError('');try{setReceipt(await api('/api/finance/payable-due/receipts/'+encodeURIComponent(receiptKey)));}catch(e){setError(e.message);}finally{setBusy(false);}}
|
|
return <section className="panel" data-sgf-operation-uncertain={form||preview||locked?'true':undefined}><h1>Vencimento administrativo de fornecedor</h1><p>Documento escolhido: {row.reference}. Condição individual revista, separada da factura e do livro monetário. Não cria pagamento, juros ou lançamento.</p><button disabled={locked||Boolean(form)} onClick={onClose}>Voltar às posições</button>{error&&<p role="alert">{error}</p>}{notice&&<p role="status">{notice}</p>}
|
|
<form data-sgf-read-filters="true" onSubmit={e=>{e.preventDefault();setQuery({reference_on:reference,page:1,per_page:25});}}><fieldset disabled={locked||Boolean(form)}><label>Referência explícita para atraso<input type="date" required value={reference} onChange={e=>setReference(e.target.value)}/></label><button>Consultar condições e histórico</button></fieldset></form>
|
|
{data&&<><DueFacts data={data}/><p>{data.notice}</p><p>Versão actual {data.basis.version} · {dueLabels[data.basis.last?.state]??'Sem preparação'}.</p>{data.capabilities.prepare&&<button disabled={locked||Boolean(form)} onClick={()=>start('prepare')}>Preparar condição individual</button>}{data.capabilities.review&&data.basis.last?.state==='prepared'&&<button disabled={locked||Boolean(form)} onClick={()=>start('review')}>Rever preparação actual</button>}
|
|
<h2>Histórico auditado</h2><div className="table-scroll"><table><thead><tr><th>Versão / estado</th><th>Condição / evidência</th><th>Autor / data / fundamento</th></tr></thead><tbody>{data.history.data.map(v=><tr key={v.id}><td>{v.version} · {dueLabels[v.state]}</td><td>{v.data.due_on??'Desconhecido'}<small>{v.data.evidence}</small></td><td>QA{v.host_actor_id} · {v.created_at}<small>{v.reason}</small></td></tr>)}</tbody></table></div><p>Página {data.history.current_page} de {data.history.last_page}, {data.history.total} evento(s).</p><button disabled={locked||Boolean(form)||data.history.current_page<=1} onClick={()=>setQuery(q=>({...q,page:q.page-1}))}>Histórico anterior</button><button disabled={locked||Boolean(form)||data.history.current_page>=data.history.last_page} onClick={()=>setQuery(q=>({...q,page:q.page+1}))}>Histórico seguinte</button><label>Eventos por página<select disabled={locked||Boolean(form)} value={query.per_page} onChange={e=>setQuery(q=>({...q,per_page:Number(e.target.value),page:1}))}><option value="25">25</option><option value="100">100</option></select></label></>}
|
|
{form&&<form onSubmit={previewForm}><fieldset disabled={locked||Boolean(preview)}><h2>{form.action==='prepare'?'Preparação individual':'Revisão distinta'} · versão observada {form.version}</h2>{form.action==='prepare'?<><label><input type="checkbox" checked={form.unknown} onChange={e=>setForm(f=>({...f,unknown:e.target.checked}))}/>Declarar explicitamente vencimento desconhecido</label><label>Data proposta<input type="date" required={!form.unknown} disabled={form.unknown} min="2000-01-01" max="2100-12-31" value={form.data.due_on} onChange={e=>setForm(f=>({...f,data:{...f.data,due_on:e.target.value}}))}/></label><label>Referência da evidência<input required minLength={10} maxLength={1000} value={form.data.evidence} onChange={e=>setForm(f=>({...f,data:{...f.data,evidence:e.target.value}}))}/></label></>:<><p>Preparação a conferir: {JSON.parse(data.basis.last.data).due_on??'Desconhecido'} · {JSON.parse(data.basis.last.data).evidence}</p><label>Decisão<select required value={form.data.decision} onChange={e=>setForm(f=>({...f,data:{decision:e.target.value}}))}><option value="">Escolher…</option><option value="approved">Aprovar condição</option><option value="refused">Recusar preparação</option></select></label></>}<label>Fundamento individual<textarea required minLength={10} maxLength={2000} value={form.reason} onChange={e=>setForm(f=>({...f,reason:e.target.value}))}/></label><button>Conferir impacto antes de confirmar</button></fieldset><button type="button" disabled={locked} onClick={()=>{setForm(null);setPreview(null);}}>Descartar formulário / prévia</button></form>}
|
|
{preview&&<section><h2>Impacto a confirmar</h2><p>Antes: v{preview.impact.before.version}, condição revista {preview.impact.before.reviewed_terms?.due_on??'Desconhecida'}. Depois: v{preview.impact.after.version}, {dueLabels[preview.impact.after.state]}, condição revista {preview.impact.after.reviewed_terms?.due_on??'Desconhecida'}.</p><p>Proposta: {preview.impact.after.proposal.due_on??'Desconhecido'} · {preview.impact.after.proposal.evidence}</p><p>Saldo mantém {minorAmount(preview.impact.balance_minor)} {preview.impact.currency}; zero pagamentos e zero lançamentos. Fundamento: {form.reason}.</p><button disabled={locked} onClick={()=>confirm()}>Confirmar esta decisão individual</button><p>Chave de recuperação: <code>{preview.key}</code></p></section>}
|
|
{operation.uncertain&&<><p role="alert">Resultado incerto. Conserve esta decisão e a chave; não prepare outra operação.</p><button disabled={busy} onClick={()=>confirm(true)}>Recuperar a mesma confirmação</button></>}
|
|
<h2>Recibo de uma operação própria</h2><label>Chave da operação<input value={receiptKey} disabled={locked||Boolean(form)} onChange={e=>setReceiptKey(e.target.value)}/></label><button disabled={locked||Boolean(form)||!receiptKey} onClick={recoverReceipt}>Consultar recibo sem executar</button>{receipt&&<p>Recibo histórico v{receipt.result.version} · {dueLabels[receipt.result.state]} · {receipt.created_at} · {receipt.reason}. Nenhuma operação executada nesta consulta.</p>}
|
|
</section>;
|
|
}
|