19 lines
11 KiB
JavaScript
19 lines
11 KiB
JavaScript
import {confirmation} from './sgf-stock-final.mjs';
|
||
const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content;
|
||
const states={imported:'Importado',statement_reviewed:'Extracto revisto',statement_refused:'Extracto recusado',match_prepared:'Correspondência proposta',match_reviewed:'Correspondência revista',match_refused:'Correspondência recusada',finalized:'Conferência finalizada'};let state,page=1,preview=null,operationKey=null,preparing=false,reading=false,readGeneration=0,dirty=false;
|
||
const money=n=>(n/100).toFixed(2),el=(tag,text,parent)=>{const e=document.createElement(tag);if(text!==null)e.textContent=text;parent.append(e);return e;},error=e=>$('message').textContent=e.message;
|
||
async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};}
|
||
const op=confirmation(async body=>{if(operationKey){const old=await api('/api/bank-reconciliation/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/bank-reconciliation/confirm',body);});
|
||
function locked(){const busy=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=busy||!state||Boolean(state.blocked)||Boolean(state.finalized)||!Object.values(state.capabilities).some(Boolean);$('filters').disabled=busy;$('refresh').disabled=busy;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;}
|
||
function input(id,label,type='text',value='',required=true){const l=el('label',label,$('inputs')),e=el(type==='textarea'?'textarea':'input',null,l);e.id=id;e.required=required;if(type!=='textarea')e.type=type;e.value=value;return e;}
|
||
function select(id,label,choices){const l=el('label',label,$('inputs')),s=el('select',null,l);s.id=id;s.required=true;s.append(new Option('Escolher…',''));for(const [value,text]of choices)s.append(new Option(text,String(value)));return s;}
|
||
function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const action=$('action').value;if(action==='import'){input('account','Conta fictícia do exercício');input('currency','Moeda');input('period','Período','month');input('opening','Abertura explícita');input('closing','Fecho explícito');input('csv','Conteúdo CSV artificial','textarea').maxLength=8192;const sample=el('button','Carregar exemplo QA aprovado',$('inputs'));sample.id='example';sample.type='button';sample.onclick=()=>{$('account').value='QA-BANCO-LOCAL';$('currency').value='MZN';$('period').value='2026-11';$('opening').value='0.00';$('closing').value='0.00';$('csv').value=state.template_csv;dirty=true;};const file=input('csvFile','Ou ler o ficheiro CSV QA','file','',false);file.accept='.csv,text/csv';file.onchange=async()=>{if(preview||op.pending)return;try{const f=file.files?.[0];if(!f)return;if(f.size>8192)throw Error('O CSV QA não pode exceder8192 bytes.');preparing=true;locked();const text=await f.text();$('csv').value=text;dirty=true;}catch(e){error(e);}finally{preparing=false;locked();}};el('p','Saldo intermédio−20 faz parte do exercício; não autoriza descoberto nem representa caixa disponível.',$('inputs'));}
|
||
if(action==='review_statement'||action==='review_match')select('decision','Decisão explícita',[['reviewed','Rever favoravelmente'],['refused','Recusar']]);if(action==='prepare_match'){select('rowReference','Linha de extracto',state.rows.filter(r=>r.status==='unmatched').map(r=>[r.reference,r.reference+' · débito '+money(r.debit_minor)+' / crédito '+money(r.credit_minor)]));select('ledgerLine','Linha contabilística autorizada',state.source_lines.map(l=>[l.id,'Diário '+l.journal_id+' · '+l.account_code+' · débito '+money(l.debit_minor)+' / crédito '+money(l.credit_minor)]));}if(action==='review_match')select('proposal','Proposta exacta por rever',state.pending_proposals.map(p=>[p.id,'v'+p.version+' · '+p.data.row_reference+' → diário '+p.data.journal_id]));if(action==='finalize')el('p','Exige duas correspondências revistas, sem propostas pendentes e diferença0. Não cria lançamentos.',$('inputs'));}
|
||
function actions(){const prior=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [value,label,right]of [['import','Importar extracto artificial','import'],['review_statement','Rever extracto importado','review'],['prepare_match','Propor correspondência individual','prepare'],['review_match','Rever correspondência exacta','review'],['finalize','Conferência final','review']])if(state.capabilities[right])$('action').append(new Option(label,value));if([...$('action').children].some(o=>o.value===prior))$('action').value=prior;fields();}
|
||
async function load(){if(preview||op.pending)throw Error('Conclua ou cancele a conferência em curso.');const generation=++readGeneration,old=JSON.stringify([state?.version,state?.capabilities]);reading=true;locked();try{const r=await api('/api/bank-reconciliation?'+new URLSearchParams({page,per_page:$('size').value,...($('status').value?{status:$('status').value}:{})}));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Consulta indisponível.');state=r.value;$('summary').replaceChildren();if(state.blocked)el('p',state.blocked,$('summary'));if(state.statement)el('p',state.statement.account_ref+' · '+state.statement.currency+' · '+state.statement.period+' · abertura '+money(state.statement.opening_minor)+' · fecho '+money(state.statement.closing_minor)+' · '+(state.finalized?'Finalizada':state.statement_reviewed?'Extracto revisto':'Por rever')+' · versão '+state.version,$('summary'));else el('p','Sem extracto importado. A ausência não representa saldo bancário zero.',$('summary'));el('p','Origem: '+(state.source_kind==='native_qa_ledger'?'duas linhas efectivas do diário QA':'origem do exercício indisponível')+'. Sem classificação bancária real ou novos efeitos financeiros.',$('summary'));$('download').replaceChildren();if(state.report_available){const a=el('a','Descarregar relatório interno QA', $('download'));a.href='/api/bank-reconciliation/report';}
|
||
$('rows').replaceChildren();for(const row of state.rows){const tr=el('tr',null,$('rows'));el('td',row.reference+' · '+row.date,tr);el('td',money(row.debit_minor),tr);el('td',money(row.credit_minor),tr);el('td',({unmatched:'Por propor',pending:'Por rever',matched:'Conferida'})[row.status]+(row.match?' → diário '+row.match.data.journal_id:''),tr);}$('events').replaceChildren();for(const row of state.history?.data??[]){const tr=el('tr',null,$('events'));el('td','v'+row.version+' · autor '+row.actor_id,tr);el('td',states[row.state]+' · '+row.created_at,tr);el('td',row.reason,tr);const td=el('td',null,tr);el('button','Consultar versão',td).onclick=()=>{if(preview||op.pending)return;$('detail').textContent=JSON.stringify({versao:row.version,anterior:row.previous_id,preparador:row.prepared_by,autor:row.actor_id,fundamento:row.reason,estado:states[row.state],dados:row.data},null,2);};}const p=state.history;$('page').textContent=p?'Página '+p.current_page+' de '+p.last_page+' · '+p.total+' eventos':'Sem histórico';$('previous').disabled=!p||page<=1;$('next').disabled=!p||page>=p.last_page;if(old!==JSON.stringify([state.version,state.capabilities]))actions();}catch(e){if(generation===readGeneration){state=null;$('summary').textContent=e.message;$('rows').replaceChildren();$('events').replaceChildren();$('download').replaceChildren();}throw e;}finally{if(generation===readGeneration){reading=false;locked();}}}
|
||
$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{const action=$('action').value;if(!action)throw Error('Escolha a acção.');let data={},proposal=null;if(action==='import')data={account_ref:$('account').value,currency:$('currency').value,period:$('period').value,opening:$('opening').value,closing:$('closing').value,csv:$('csv').value};if(action==='review_statement'||action==='review_match')data={decision:$('decision').value};if(action==='prepare_match')data={row_reference:$('rowReference').value,ledger_line_id:Number($('ledgerLine').value)};if(action==='review_match')proposal=Number($('proposal').value);operationKey=crypto.randomUUID();const body={action,version:state.version,data,reason:$('reason').value,...(proposal?{proposal_id:proposal}:{})};const r=await api('/api/bank-reconciliation/preview',body,operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;el('p','Versão anterior '+i.before_version+' → '+i.after_version+' · '+states[i.state],$('impact'));if(i.data?.row_reference)el('p',i.data.row_reference+' → diário '+i.data.journal_id+' / linha '+i.data.ledger_line_id,$('impact'));if(i.existing_statement)el('p','Mesmo extracto já importado; sem novo evento ou linhas.',$('impact'));el('p','Pagamentos/lançamentos novos: nenhum. Operação bancária real: não.',$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}};
|
||
async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;$('impact').replaceChildren();$('receipt').textContent='Versão '+r.version+' · '+states[r.state]+(r.replayed?' · recuperada sem duplicação':' · registada');await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}}
|
||
$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/bank-reconciliation/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo confirmado desta função para a referência.');$('receipt').textContent='Recibo histórico: versão '+r.value.result.version+' · '+states[r.value.result.state]+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}};
|
||
$('action').onchange=fields;$('operation').oninput=()=>{dirty=true;};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('refresh').onclick=()=>load().catch(error);$('apply').onclick=()=>{page=1;load().catch(error);};$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error);
|