Files
SGF/public/stock-operations-workspace.mjs
2026-11-11 09:00:17 +02:00

23 lines
8.7 KiB
JavaScript

const root=document.querySelector('#stock-workspace'),form=document.querySelector('#operation'),fields=document.querySelector('#fields');
const status=document.querySelector('#status'),impactSection=document.querySelector('#impact-section'),confirm=document.querySelector('#confirm');
let busy=false,preview=null,uncertain=false,dirty=false,epoch=0,historyPage=1;
const pickers=[];
const node=(tag,text)=>{const el=document.createElement(tag);if(text!==undefined)el.textContent=text;return el;};
function message(text){status.textContent=text;}
function lock(value){busy=value;fields.disabled=value||uncertain;confirm.disabled=value;document.querySelectorAll('button').forEach(b=>{if(!fields.contains(b)&&b!==confirm)b.disabled=value||uncertain;});}
async function api(path,data){const r=await fetch('/api/stock-operations'+path,{method:data?'POST':'GET',headers:{Accept:'application/json','X-SGF-Context':root.dataset.context,'X-CSRF-TOKEN':root.dataset.csrf,...(data?{'Content-Type':'application/json'}:{})},...(data?{body:JSON.stringify(data)}:{})});let body;try{body=await r.json();}catch{body={};}if(!r.ok){const error=Error(body.message||'Operação indisponível.');error.status=r.status;throw error;}return body;}
function invalidate(){if(uncertain)return;epoch++;preview=null;impactSection.hidden=true;}
function selected(){const d=Object.fromEntries(new FormData(form));for(const p of pickers){if(p.wrap.hidden||p.name==='equipment_id'&&!d[p.name])delete d[p.name];else d[p.name]=Number(d[p.name]);}if(d.kind!=='transfer')delete d.destination_warehouse_id;if(d.kind!=='issue')delete d.destination;return d;}
async function loadPicker(p,page=1){const request=++p.epoch;if(p.view==='counts'&&(!form.elements.stock_item_id.value||!form.elements.warehouse_id.value))return;const filters={view:p.view,page,search:p.search.value};if(p.view==='counts'){filters.stock_item_id=form.elements.stock_item_id.value;filters.warehouse_id=form.elements.warehouse_id.value;}const value=await api('?'+new URLSearchParams(filters));if(request!==p.epoch)return;p.page=value.current_page;p.next.disabled=!value.next_page_url;p.previous.disabled=page<=1;const selected=p.select.selectedOptions[0];const remembered=selected?.value?selected.cloneNode(true):null;p.select.replaceChildren(node('option','Seleccione explicitamente'));p.select.options[0].value='';if(remembered)p.select.append(remembered);for(const row of value.data){if(remembered&&remembered.value===String(row.id))continue;const option=node('option',`${row.code?row.code+' · ':''}${row.name}${row.unit?' ('+row.unit+')':''}`);option.value=row.id;p.select.append(option);}if(remembered)p.select.value=remembered.value;}
for(const [name,label,view] of [['warehouse_id','Armazém de origem','warehouses'],['destination_warehouse_id','Armazém de destino','warehouses'],['stock_item_id','Artigo','items'],['responsible_id','Responsável','people'],['equipment_id','Equipamento de destino (quando exigido)','equipment'],['count_id','Contagem física por ajustar','counts']]){
const wrap=node('div'),search=node('input'),select=node('select'),next=node('button','Mais resultados'),previous=node('button','Resultados anteriores'),find=node('button','Pesquisar');search.placeholder='Pesquisar '+label.toLowerCase();search.setAttribute('aria-label',search.placeholder);select.name=name;select.required=name!=='equipment_id';for(const b of [next,previous,find])b.type='button';const title=node('label',label);title.append(select);wrap.append(title,search,find,previous,next);document.querySelector('#pickers').append(wrap);const p={name,view,wrap,search,select,next,previous,page:1,epoch:0};pickers.push(p);find.onclick=()=>loadPicker(p).catch(e=>message(e.message));next.onclick=()=>loadPicker(p,p.page+1).catch(e=>message(e.message));previous.onclick=()=>loadPicker(p,p.page-1).catch(e=>message(e.message));
}
function kind(){const k=form.elements.kind.value;for(const p of pickers){const visible=p.name==='destination_warehouse_id'?k==='transfer':p.name==='equipment_id'?k==='issue':p.name==='count_id'?k==='adjustment':true;p.wrap.hidden=!visible;p.select.disabled=!visible;p.select.required=visible&&p.name!=='equipment_id';}document.querySelector('#destination-field').hidden=k!=='issue';form.elements.destination.required=k==='issue';form.elements.destination.disabled=k!=='issue';document.querySelector('#quantity-field').hidden=k==='adjustment';form.elements.quantity.disabled=k==='adjustment';form.elements.quantity.required=k!=='adjustment';}
form.addEventListener('input',()=>{dirty=true;invalidate();});form.addEventListener('change',e=>{dirty=true;invalidate();kind();if(['kind','warehouse_id','stock_item_id'].includes(e.target?.name)){const p=pickers.find(p=>p.name==='count_id');p.select.value='';if(!p.wrap.hidden)loadPicker(p).catch(e=>message(e.message));}});
form.onsubmit=async e=>{e.preventDefault();if(busy||uncertain)return;invalidate();const stamp=epoch,data=selected();lock(true);message('A conferir o saldo e o impacto…');try{const basisKeys=['kind','warehouse_id','stock_item_id','responsible_id','destination_warehouse_id','count_id'];const basis=await api('/basis?'+new URLSearchParams(Object.fromEntries(Object.entries(data).filter(([k])=>basisKeys.includes(k)))));if(data.kind==='count')data.ledger_version=basis.basis.balances.find(b=>b.warehouse_id===data.warehouse_id).version;const result=await api('/preview',{operation_key:crypto.randomUUID(),revision:basis.revision,data});if(stamp!==epoch)return;preview=result.preview;const dl=document.querySelector('#impact');dl.replaceChildren();for(const [label,value] of [['Operação',({transfer:'Transferência física',issue:'Saída para utilização',count:'Registar contagem física',adjustment:'Aplicar diferença da contagem'})[data.kind]],['Artigo',basis.basis.item.name],['Quantidade',`${data.kind==='adjustment'?result.impact.quantity_milli/1000:data.quantity} ${basis.basis.item.unit}`],['Origem',basis.basis.warehouses.find(w=>w.id===data.warehouse_id)?.name],['Destino',data.kind==='transfer'?basis.basis.warehouses.find(w=>w.id===data.destination_warehouse_id)?.name:data.kind==='issue'?data.destination:'Mesmo armazém'],['Saldo físico anterior',String(result.impact.theoretical_milli/1000)+' '+basis.basis.item.unit],['Variação na origem',String(result.impact.change_milli/1000)+' '+basis.basis.item.unit],['Saldo físico posterior',String(result.impact.quantity_after_milli/1000)+' '+basis.basis.item.unit],...(data.kind==='transfer'?[['Saldo posterior no destino',String(result.impact.destination_quantity_after_milli/1000)+' '+basis.basis.item.unit]]:[]),['Responsável',basis.basis.responsible.name],['Referência',result.impact.reference],['Fundamento',result.impact.reason],['Contabilidade','Sem lançamento contabilístico']])dl.append(node('dt',label),node('dd',String(value)));impactSection.hidden=false;message('Confira os dados antes de confirmar.');}catch(error){message(error.message);}finally{lock(false);}};
confirm.onclick=async()=>{if(busy||!preview)return;lock(true);message(uncertain?'A recuperar a mesma operação…':'A confirmar…');try{const result=await api('/confirm',{preview});uncertain=false;preview=null;impactSection.hidden=true;dirty=false;form.reset();kind();message(`Operação ${result.reference} registada${result.replayed?' (recibo recuperado)':''}.`);historyPage=1;await history();}catch(error){if(!error.status||error.status>=500){uncertain=true;message('Resultado ainda por confirmar. Use o mesmo botão para recuperar; os campos ficam protegidos.');}else{uncertain=false;invalidate();message(error.message);}}finally{lock(false);}};
async function history(){const value=await api('?'+new URLSearchParams({view:'history',page:historyPage}));const container=document.querySelector('#history');const table=node('table'),head=node('tr');for(const text of ['Referência','Operação','Quantidade','Data'])head.append(node('th',text));table.append(head);for(const row of value.data){const tr=node('tr');for(const text of [row.reference,({transfer:'Transferência',issue:'Saída',count:'Contagem',adjustment:'Ajuste'})[row.kind]||row.kind,String(row.quantity_milli/1000),row.recorded_at])tr.append(node('td',text));table.append(tr);}container.replaceChildren(table);document.querySelector('#older').disabled=!value.next_page_url;document.querySelector('#newer').disabled=historyPage<=1;}
document.querySelector('#refresh').onclick=()=>{historyPage=1;history().catch(e=>message(e.message));};document.querySelector('#older').onclick=()=>{historyPage++;history().catch(e=>message(e.message));};document.querySelector('#newer').onclick=()=>{historyPage=Math.max(1,historyPage-1);history().catch(e=>message(e.message));};
window.addEventListener('beforeunload',e=>{if(uncertain||preview||busy||dirty){e.preventDefault();e.returnValue='';}});
kind();Promise.all([...pickers.map(p=>loadPicker(p)),history()]).catch(e=>message(e.message));