Files
SGF/public/supplier-invoice-workspace.mjs
2026-11-04 09:37:20 +02:00

16 lines
8.8 KiB
JavaScript

import {rememberFocus} from './purchasing-focus.mjs';
import {evidence as renderEvidence,purchasingStatus} from './purchasing-presenter.mjs';
import {appendInvoiceMatching,pendingQuantityWarnings,quantity} from './invoice-matching.mjs';
import {beginWorkspace,focusHref,appendStageLinks} from './purchasing-focus.mjs';
const root=document.querySelector('#supplier-invoices'),status=document.querySelector('#status'),orders=document.querySelector('#orders'),detail=document.querySelector('#detail');let page=1,last=1,turn=0,busy=false;
const n=(tag,text)=>{const e=document.createElement(tag);if(text!==undefined)e.textContent=text;return e;},money=v=>(Number(v)/100).toFixed(2)+' MZN';
async function api(path,data){const r=await fetch('/api/purchasing-invoices'+path,{method:data===undefined?'GET':'POST',headers:{Accept:'application/json','X-SGF-Context':root.dataset.context,'X-CSRF-TOKEN':root.dataset.csrf,...(data===undefined?{}:{'Content-Type':'application/json'})},body:data===undefined?undefined:JSON.stringify(data)});const d=await r.json();if(!r.ok)throw Error(Object.values(d.errors||{}).flat().join(' ')||d.message||'Não foi possível concluir.');return d;}
function field(form,label,type='text'){const l=n('label',label),e=n(type==='textarea'?'textarea':'input');if(type!=='textarea')e.type=type;e.required=true;l.append(e);form.append(l);return e;}
function command(id,revision,action,title,build){const own=turn,form=n('form'),impact=n('section'),prepare=n('button','Conferir impacto'),confirm=n('button','Confirmar esta operação');let key=crypto.randomUUID(),version=0,cut=null;form.append(n('h2',title));const read=build(form),reason=field(form,'Fundamento desta conferência','textarea');reason.minLength=10;reason.maxLength=2000;prepare.type='submit';confirm.type='button';confirm.disabled=true;impact.hidden=true;form.oninput=()=>{version++;key=crypto.randomUUID();cut=null;confirm.disabled=true;impact.hidden=true;};form.onsubmit=async e=>{e.preventDefault();if(busy)return;busy=true;prepare.disabled=true;const observed=version;try{const p=await api('/orders/'+id+'/preview',{operation_key:key,revision,action,reason:reason.value,data:read()});if(own!==turn||observed!==version)return;cut=p.preview;impact.replaceChildren(n('h3',p.impact.operation),n('p','Documento: '+p.impact.invoice.number+' · estado resultante: '+p.impact.invoice.status),n('p','Total conferido: '+money(p.impact.invoice.total_minor)),n('p','Consumo do compromisso nesta operação: '+money(p.impact.budget_consumption_minor)),n('p','Fundamento: '+p.impact.reason),renderEvidence(p.impact.invoice.items),n('p','A confirmação volta a conferir a base actual. Não cria obrigação, pagamento, movimento físico ou lançamento contabilístico.'));impact.hidden=false;confirm.disabled=false;}catch(e){status.textContent=e.message;}finally{busy=false;prepare.disabled=false;}};confirm.onclick=async()=>{if(busy||!cut)return;busy=true;confirm.disabled=true;try{const committed=await api('/orders/'+id+'/confirm',{preview:cut});if(own===turn)await open(id,committed.invoice_id??null);}catch(e){status.textContent=e.message+' Se perdeu a resposta, repita a mesma confirmação para recuperar o resultado sem duplicar.';confirm.disabled=false;}finally{busy=false;}};form.append(prepare,impact,confirm);detail.append(form);}
async function load(){const own=++turn;try{const d=await api('?page='+page);if(own!==turn)return;orders.replaceChildren();for(const order of d.data){const b=n('button',order.number+' · '+order.status);b.onclick=()=>open(order.id);orders.append(b);}if(!d.data.length)orders.append(n('p','Aguarda uma ordem emitida no percurso autorizado.'));last=d.last_page;document.querySelector('#page').textContent=`Página ${page} de ${last}`;document.querySelector('#previous').disabled=page<=1;document.querySelector('#next').disabled=page>=last;}catch(e){status.textContent=e.message;}}
async function open(id,focusedInvoice=null){const own=++turn;detail.replaceChildren();status.textContent='A consultar ordem e recepção…';try{const d=await api('/orders/'+id);if(own!==turn)return;if(focusedInvoice!==null&&!d.invoices.some(v=>v.id===focusedInvoice))throw Error('A factura indicada não pertence a esta ordem. Seleccione um documento autorizado.');rememberFocus('invoices',{order:id,...(focusedInvoice?{invoice:focusedInvoice}:{})});status.textContent='';const summary=n('section');summary.append(n('h2',d.order.number),n('p','Moeda: '+d.order.currency),n('p','Revisao da conferencia: '+d.revision.slice(0,12)));const progress=n('details');progress.append(n('summary','Recepção e quantidades de origem'),renderEvidence(d.receipt_progress),renderEvidence(d.physical_corrections));summary.append(progress);for(const line of d.order.items)summary.append(n('p',`${line.description} · ${line.quantity} ${line.unit} · preço ${money(line.unit_price_minor)} · imposto documental ${line.tax_rate}%`));detail.append(summary);if(d.matching_progress)appendInvoiceMatching(summary,d.matching_progress);appendStageLinks(root,summary,[{href:focusHref('receipts',{order:id}),label:'Consultar recepções desta ordem'},...(Number.isSafeInteger(d.order.requisition_id)?[{href:focusHref('dossiers',{request:d.order.requisition_id}),label:'Consultar dossiê desta requisição'}]:[])]);
if(d.can_prepare)command(id,d.revision,'register','Registar documento recebido',form=>{const number=field(form,'Número da factura recebida'),date=field(form,'Data de emissão indicada no documento','date'),subtotal=field(form,'Base documental MZN'),tax=field(form,'Imposto documental MZN'),total=field(form,'Total documental MZN');number.maxLength=100;form.append(n('p','Seleccione explicitamente as linhas e quantidades do documento. Preço, unidade e imposto de origem são conferidos pelo workflow.'));const lines=d.order.items.map(line=>{const block=n('article'),select=n('input');select.type='checkbox';const label=n('label',line.description);label.append(select);block.append(label);const matching=d.matching_progress?.find(v=>v.line_id===line.id);if(matching)block.append(n('p','Restante actual para conferir: '+quantity(matching.remaining_milli)+' '+matching.unit));const billedQuantity=field(block,'Quantidade facturada — até3 casas decimais');billedQuantity.required=false;billedQuantity.disabled=true;select.onchange=()=>{billedQuantity.disabled=!select.checked;billedQuantity.required=select.checked;};form.append(block);return {line,select,quantity:billedQuantity};});return ()=>({number:number.value,issued_on:date.value,subtotal:subtotal.value,tax:tax.value,total:total.value,items:lines.filter(x=>x.select.checked).map(x=>({purchase_order_item_id:x.line.id,quantity:x.quantity.value}))});});
for(const invoice of d.invoices){const panel=n('section');if(invoice.id===focusedInvoice){panel.dataset.selected='true';panel.append(n('p','Documento indicado no atalho'));}panel.append(n('h2',invoice.number+' · '+invoice.status),n('p','Data documental: '+invoice.issued_on+' · total '+money(invoice.total_minor)),n('p','Preparador #'+invoice.created_by+' · validador #'+(invoice.validated_by??'pendente')),renderEvidence(invoice.items));detail.append(panel);if(d.matching_progress)for(const warning of pendingQuantityWarnings(invoice,d.matching_progress)){const note=n('p',warning);note.setAttribute('role','note');panel.append(note);}appendStageLinks(root,panel,[{href:focusHref('obligations',{invoice:invoice.id}),label:'Consultar obrigação desta factura'}]);if(invoice.can_validate)command(id,d.revision,'validate','Validar conferência de '+invoice.number,form=>{form.append(n('p','Confirma a versão registada, com as recepções e facturas já validadas. Consome '+money(invoice.total_minor)+' no compromisso existente, uma única vez.'));return ()=>({invoice_id:invoice.id});});}
const history=n('section');detail.append(history);let historySerial=0;const historyPage=async current=>{const request=++historySerial;try{const h=await api('/orders/'+id+'/history?page='+current);if(own!==turn||request!==historySerial)return;history.replaceChildren(n('h2','Histórico da conferência'));for(const entry of h.data){const row=n('details');row.append(n('summary',entry.created_at+' · '+entry.action+' · autor #'+entry.user_id),renderEvidence({anterior:entry.before,posterior:entry.after}));history.append(row);}history.append(n('p','Página '+h.current_page+' de '+h.last_page));for(const [enabled,p,label]of [[current>1,current-1,'Anterior'],[current<h.last_page,current+1,'Seguinte']])if(enabled){const b=n('button',label);b.onclick=()=>historyPage(p);history.append(b);}}catch(e){if(own===turn&&request===historySerial)status.textContent=e.message;}};await historyPage(1);}catch(e){if(own===turn)status.textContent=e.message;}}
document.querySelector('#previous').onclick=()=>{if(page>1){page--;load();}};document.querySelector('#next').onclick=()=>{if(page<last){page++;load();}};beginWorkspace({stage:'invoices',root,load:load,open:f=>open(f.order,f.invoice??null),current:()=>turn,error:text=>{status.textContent=text;}});