26 lines
16 KiB
JavaScript
26 lines
16 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={prepared:'Preparado',reviewed:'Revisto',refused:'Recusado',withdrawn:'Retirado',determined_qa:'Calculado QA',pending:'Pendente',pending_representation:'Representação pendente'};let state,page=1,selected=null,preview=null,operationKey=null,preparing=false,reading=false,readGeneration=0,dirty=false,documentRowCounter=0;
|
||
const el=(tag,text,parent)=>{const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;};const 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/fiscal-rules/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/fiscal-rules/confirm',body);});
|
||
function locked(){const busy=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=busy||!state||(!state.capabilities.prepare&&!state.capabilities.review);$('filters').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')),i=el(type==='textarea'?'textarea':'input',null,l);i.id=id;if(type!=='textarea')i.type=type;i.value=value;i.required=required;return i;}
|
||
function select(id,label,choices){const l=el('label',label,$('inputs')),s=el('select',null,l);s.id=id;s.required=true;for(const [v,t]of choices)s.append(new Option(t,v));return s;}
|
||
function documentLine(){if(preview||op.pending||preparing)return;const table=$('documentLines');if(table.children.length>=25){error(Error('Máximo25 linhas.'));return;}const id=++documentRowCounter,tr=el('tr',null,table);tr.dataset.rowId=String(id);for(const [name,label]of [['reference','Referência'],['quantity','Quantidade'],['price','Preço unitário']]){const td=el('td',null,tr),i=el('input',null,td);i.id='doc-'+name+'-'+id;i.required=true;i.type='text';i.maxLength=name==='reference'?80:25;i.setAttribute('aria-label',label+' linha '+id);}const td=el('td',null,tr),b=el('button','Retirar linha',td);b.type='button';b.onclick=()=>{if(preview||op.pending||preparing)return;if(table.children.length<=1){error(Error('Mantenha pelo menos uma linha.'));return;}tr.remove();dirty=true;};}
|
||
function documentFields(){el('p',selected?'Regra escolhida: '+selected.rule_key+' · versão '+selected.rule_version:'Sem regra escolhida: resultado pendente, sem taxa zero por omissão.',$('inputs'));select('documentMode','Modalidade explícita',[['','Escolher…'],['net_added','Preço sem imposto'],['gross_included','Preço com imposto incluído']]);input('documentOn','Data para conferir a vigência','date','');select('documentCurrency','Moeda',[['','Escolher…'],['MZN','MZN']]);el('p','Política: imposto truncado por linha; no incluído, base documental = bruto − imposto documental. Exactos e ambas as diferenças preservados. Totais somam as linhas.',$('inputs'));const wrap=el('div',null,$('inputs')),table=el('table',null,wrap),head=el('thead',null,table),tr=el('tr',null,head);for(const title of ['Referência','Quantidade','Preço unitário','Linha'])el('th',title,tr);const body=el('tbody',null,table);body.id='documentLines';documentRowCounter=0;documentLine();const add=el('button','Adicionar linha', $('inputs'));add.id='addDocumentLine';add.type='button';add.onclick=()=>{documentLine();dirty=true;};}
|
||
function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const action=$('action').value;if(!action)return;
|
||
if(action==='prepare'){input('ruleKey','Chave da regra (nova chave ou sucessora)','text',selected?.rule_key??'').maxLength=80;const template=select('template','Definição a preparar',[['','Escolher…'],...state.fixtures.cases.map(c=>[c.fixture_id,c.definition.label]),['pending','Referência ainda não conferida']]);const container=el('div',null,$('inputs'));container.id='templateFields';template.onchange=()=>{container.replaceChildren();if(template.value==='pending'){const l=el('label','Nome da referência',container),name=el('input',null,l);name.id='metadataLabel';name.required=true;const lf=el('label','Família',container),family=el('select',null,lf);family.id='metadataFamily';family.required=true;for(const [v,t]of [['','Escolher…'],['iva','IVA'],['irps','IRPS'],['irpc','IRPC'],['ispc','ISPC'],['ice','ICE'],['inss','INSS'],['other','Outros']])family.append(new Option(t,v));for(const [id,title]of [['sourceReference','Referência da fonte'],['conditions','Condições conhecidas ou lacunas']]){const label=el('label',title,container),i=el('textarea',null,label);i.id=id;i.required=true;}}else{const d=state.fixtures.cases.find(c=>c.fixture_id===template.value)?.definition;if(d)el('p','Família QA artificial · MZN · taxa de ensaio '+d.parameters.rate+' × base · vigência '+d.valid_from+' a '+(d.valid_until??'sem termo neste caso')+' · representação em duas casas, truncagem. Fonte '+d.source_reference+'.',container);}};}
|
||
if(action==='calculate_document')documentFields();
|
||
if(action==='review'||action==='withdraw'){el('p',selected?'Versão escolhida: '+selected.rule_key+' · v'+selected.rule_version+' · revisão actual '+selected.current_revision:'Escolha explicitamente uma versão no catálogo antes da decisão.',$('inputs'));if(action==='review')select('decision','Decisão',[['','Escolher…'],['reviewed','Rever favoravelmente'],['refused','Recusar']]);}
|
||
if(action==='calculate'){el('p',selected?'Regra escolhida: '+selected.rule_key+' · versão '+selected.rule_version:'Sem regra escolhida: o resultado será uma pendência, nunca taxa zero.',$('inputs'));input('base','Base decimal explícita','text','');input('on','Data para verificar vigência','date','');select('currency','Moeda',[['','Escolher…'],['MZN','MZN'],['USD','USD (incompatível neste ensaio)']]);}}
|
||
function actions(){const before=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [a,label,right]of [['prepare','Preparar regra/fonte','prepare'],['review','Rever versão escolhida','review'],['withdraw','Retirar versão revista','review'],['calculate','Calcular com versão escolhida','prepare'],...(state.document_enabled?[['calculate_document','Calcular por linhas / imposto incluído','prepare']]:[])])if(state.capabilities[right])$('action').append(new Option(label,a));if([...$('action').children].some(o=>o.value===before))$('action').value=before;fields();}
|
||
async function choose(id,type){if(preview||op.pending||preparing||reading)return;try{const r=await api('/api/fiscal-rules/records/'+id+'?type='+type);if(r.status!==200)throw Error(r.value.message??'Versão indisponível.');$('detail').textContent=JSON.stringify(r.value,null,2);$('documentDownload').replaceChildren();if(type==='calculation'&&r.value.result.price_mode){const a=el('a','Descarregar mapa interno por linhas',$('documentDownload'));a.href='/api/fiscal-rules/documents/'+id+'/report';}if(type==='rule'){selected=r.value;$('selection').textContent='Escolhida '+selected.rule_key+' · v'+selected.rule_version+' · '+states[selected.state]+(selected.id!==selected.current_version_event_id?' · versão já alterada':'');if(['review','withdraw','calculate','calculate_document'].includes($('action').value))fields();}else{selected=null;if(['calculate','calculate_document'].includes($('action').value))fields();$('selection').textContent='Cálculo histórico escolhido; a regra de outro registo não é seleccionada automaticamente.';}}catch(e){error(e);}}
|
||
async function load(){if(op.pending||preview)throw Error('Conclua ou cancele a conferência em curso.');const generation=++readGeneration,oldCapabilities=JSON.stringify(state?.capabilities);reading=true;locked();try{const view=$('view').value,query={view,page,per_page:$('size').value};if(view!=='calculations'){if($('filterKey').value)query.rule_key=$('filterKey').value;if($('family').value)query.family=$('family').value;if($('filterState').value)query.state=$('filterState').value;if($('search').value)query.search=$('search').value;}if(view==='history'&&!query.rule_key)throw Error('Indique a chave para consultar o seu histórico.');const r=await api('/api/fiscal-rules?'+new URLSearchParams(query));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Catálogo indisponível.');state=r.value;$('events').replaceChildren();for(const row of state.records.data){const tr=el('tr',null,$('events')),isCalc=view==='calculations';el('td',isCalc?'Cálculo #'+row.id:row.rule_key+' · '+row.definition.label,tr);el('td',isCalc?states[row.result.state]:'v'+row.rule_version+' / r'+row.revision+' · '+states[row.state],tr);el('td',isCalc?'Imposto: '+(row.result.tax_document??'indeterminado')+' · total: '+(row.result.total??'indeterminado'):row.definition.source_reference+' · '+(row.definition.valid_from??'vigência desconhecida')+' a '+(row.definition.valid_until??'sem termo')+' · '+(row.definition.source_status==='qa_checked'?'fonte artificial conferida':'fonte pendente'),tr);const td=el('td',null,tr);el('button',isCalc?'Consultar cálculo':'Escolher esta versão',td).onclick=()=>choose(row.id,isCalc?'calculation':'rule');if(row.command_reference)el('small','Referência: '+row.command_reference,td);}const p=state.records;$('page').textContent='Página '+p.current_page+' de '+p.last_page+' · '+p.total+' registos';$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;if(oldCapabilities!==JSON.stringify(state.capabilities))actions();}catch(e){if(generation===readGeneration){state=null;selected=null;$('events').replaceChildren();$('detail').textContent='';$('selection').textContent='Consulta não conferida; reabra o contexto autorizado.';}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.');const data={};let body={action,revision:0,data,reason:$('reason').value};if(action==='prepare'){const key=$('ruleKey').value;if(!/^[A-Z0-9_-]{3,80}$/.test(key))throw Error('Indique uma chave válida.');const stream=await api('/api/fiscal-rules?'+new URLSearchParams({view:'history',rule_key:key,per_page:25}));if(stream.status!==200)throw Error(stream.value.message??'Versão da regra não conferida.');body.rule_key=key;body.revision=stream.value.stream_revision;const template=$('template').value,c=state.fixtures.cases.find(c=>c.fixture_id===template);if(c)body.data={...c.definition,source_sha256:state.fixtures.sha256};else if(template==='pending')body.data={label:$('metadataLabel').value,family:$('metadataFamily').value,operation:'metadata_only',conditions:$('conditions').value,currency:null,unit:'none',precision:2,rounding:'DOWN',priority:0,source_reference:$('sourceReference').value,source_sha256:null,source_status:'pending',valid_from:null,valid_until:null,parameters:{},expression:{}};else throw Error('Escolha a definição explicitamente.');}
|
||
else if(action==='calculate_document')body.data={rule_event_id:selected?.id??null,currency:$('documentCurrency').value,date:$('documentOn').value,price_mode:$('documentMode').value,lines:[...$('documentLines').children].map(row=>({reference:$('doc-reference-'+row.dataset.rowId).value,quantity:$('doc-quantity-'+row.dataset.rowId).value,unit_price:$('doc-price-'+row.dataset.rowId).value}))};else if(action==='calculate')body.data={rule_event_id:selected?.id??null,base:$('base').value,date:$('on').value,currency:$('currency').value};else{if(!selected)throw Error('Escolha uma versão no catálogo.');body.rule_key=selected.rule_key;body.revision=selected.current_revision;body.target_id=selected.id;body.data=action==='review'?{decision:$('decision').value}:{};}
|
||
operationKey=crypto.randomUUID();const r=await api('/api/fiscal-rules/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;if(i.result){if(i.result.price_mode){for(const [k,v]of [['Estado',states[i.result.state]],['Base documental',i.result.base_document??'indeterminada'],['Imposto documental',i.result.tax_document??'indeterminado'],['Total',i.result.total??'indeterminado'],['Diferença da base (fracção)',i.result.base_difference_fraction??'indeterminada'],['Diferença do imposto (fracção)',i.result.tax_difference_fraction??'indeterminada']])el('p',k+': '+v,$('impact'));const table=el('table',null,$('impact')),head=el('thead',null,table),h=el('tr',null,head);for(const title of ['Linha','Base exacta','Imposto exacto','Base documental','Imposto documental','Total','Diferenças base / imposto'])el('th',title,h);const body=el('tbody',null,table);for(const line of i.result.lines){const row=el('tr',null,body);for(const v of [line.reference,line.base_exact_fraction,line.tax_exact_fraction,line.base_document??'indeterminada',line.tax_document,line.total_document??'indeterminado',(line.base_difference_fraction??'indeterminada')+' / '+line.tax_difference_fraction])el('td',v,row);}}else for(const [k,v]of [['Resultado',states[i.result.state]],['Base',i.result.base??body.data.base],['Imposto exacto',i.result.tax_exact??'indeterminado'],['Imposto documental',i.result.tax_document??'indeterminado'],['Diferença preservada',i.result.difference??'indeterminada'],['Total',i.result.total??'indeterminado']])el('p',k+': '+v,$('impact'));for(const p of i.result.pending)el('p',p,$('impact'));if(i.result.steps)el('pre',JSON.stringify(i.result.steps,null,2),$('impact'));}else{el('p','Estado anterior: '+(states[i.before.state]??'sem preparação')+'; posterior: '+states[i.after.state]+' · versão '+i.after.rule_version+' · revisão '+i.after.revision,$('impact'));el('p','Fonte: '+i.definition.source_reference+'; conferida apenas para ensaio artificial: '+(i.source_checked?'sim':'não'),$('impact'));el('p','Vigência: '+(i.definition.valid_from??'desconhecida')+' a '+(i.definition.valid_until??'sem termo'),$('impact'));}el('p','Novos lançamentos: nenhum. Activação legal: não. Documentos existentes: inalterados.',$('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;selected=null;$('selection').textContent='Escolha uma versão para a próxima operação.';$('impact').replaceChildren();$('receipt').textContent=(r.type==='rule'?'Regra revisão '+r.revision:'Cálculo #'+r.id)+' · '+states[r.state]+(r.replayed?' · recuperado sem duplicação':' · registado');fields();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/fiscal-rules/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo desta função para a referência.');$('receipt').textContent='Recibo histórico '+r.value.result.type+' #'+r.value.result.id+'. 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();};$('apply').onclick=()=>{page=1;load().catch(error);};$('refresh').onclick=()=>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);
|