Files
2026-11-11 09:00:17 +02:00

19 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);