16 lines
8.0 KiB
JavaScript
16 lines
8.0 KiB
JavaScript
import {continuationHref} from './purchasing-focus.mjs';
|
|
import {rememberFocus} from './purchasing-focus.mjs';
|
|
import {evidence as renderEvidence,purchasingStatus} from './purchasing-presenter.mjs';
|
|
import {mountRequestHistory} from './request-history.mjs';
|
|
import {beginWorkspace,focusHref,appendProcessLinks} from './purchasing-focus.mjs';
|
|
const root=document.querySelector('#requests'),status=document.querySelector('#status'),list=document.querySelector('#list'),detail=document.querySelector('#detail');let page=1,last=1,turn=0,busy=false,newRevision='',centres=[];
|
|
const n=(tag,text)=>{const e=document.createElement(tag);if(text!==undefined)e.textContent=text;return e;};
|
|
async function api(path,data){const r=await fetch('/api/purchasing-requests'+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,value='',type='text'){const wrap=n('label',label),e=n(type==='textarea'?'textarea':'input');if(type!=='textarea')e.type=type;e.required=true;e.value=value??'';wrap.append(e);form.append(wrap);return e;}
|
|
function select(form,label,options,value=''){const wrap=n('label',label),e=n('select');e.required=true;e.append(new Option('Seleccione explicitamente',''));for(const [v,t]of options)e.append(new Option(t,String(v)));e.value=String(value);wrap.append(e);form.append(wrap);return e;}
|
|
function command(id,revision,action,title,build){const own=turn,form=n('form'),prepare=n('button','Conferir impacto'),confirm=n('button','Confirmar esta operação'),impact=n('section');let cut=null,version=0,key=crypto.randomUUID();form.append(n('h2',title));const data=build(form);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('/'+id+'/preview',{operation_key:key,revision,action,data:data()});if(own!==turn||observed!==version)return;cut=p.preview;impact.replaceChildren(n('h3','Impacto antes da confirmação'),renderEvidence(p.impact));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 d=await api('/'+id+'/confirm',{preview:cut});if(own===turn){await load();await open(d.request_id);}}catch(e){status.textContent=e.message+' Pode repetir a mesma confirmação se perdeu a resposta; não duplica uma operação registada.';confirm.disabled=false;}finally{busy=false;}};form.append(prepare,impact,confirm);detail.append(form);}
|
|
function editor(id,revision,r={}){command(id,revision,'save',id?'Alterar rascunho':'Preparar requisição',form=>{const centre=select(form,'Centro de custo autorizado',centres.map(c=>[c.id,c.code+' · '+c.name]),r.cost_center_id),priority=select(form,'Prioridade',[['normal','Normal'],['high','Alta'],['urgent','Urgente']],r.priority),needed=field(form,'Necessário em',(r.needed_by??'').slice(0,10),'date'),reason=field(form,'Fundamento da necessidade',r.justification,'textarea'),area=n('div'),rows=[];form.append(area);const add=(line={})=>{if(rows.length>=50)return;const block=n('article'),values={};for(const [key,label]of [['description','Descrição'],['category','Categoria'],['unit','Unidade'],['quantity','Quantidade — até 3 casas decimais'],['unit_price','Preço estimado MZN — até 2 casas decimais'],['tax_rate','Taxa documental de ensaio (%)']])values[key]=field(block,label,line[key]??(key==='unit_price'&&line.unit_price_minor!==undefined?(line.unit_price_minor/100).toFixed(2):''));const kind=select(block,'Tipo de aquisição',[['consumable','Consumível'],['service','Serviço'],['other','Outro']],line.purchase_type);const item={block,read:()=>({...Object.fromEntries(Object.entries(values).map(([k,e])=>[k,e.value])),purchase_type:kind.value})};const remove=n('button','Retirar linha');remove.type='button';remove.onclick=()=>{rows.splice(rows.indexOf(item),1);block.remove();form.dispatchEvent(new Event('input'));};block.append(remove);rows.push(item);area.append(block);};for(const line of r.items?.length?r.items:[{}])add(line);const more=n('button','Adicionar linha');more.type='button';more.onclick=()=>{add();form.dispatchEvent(new Event('input'));};form.append(more);return ()=>({cost_center_id:Number(centre.value),priority:priority.value,needed_by:needed.value,justification:reason.value,items:rows.map(row=>row.read())});});}
|
|
async function load(){const own=++turn;try{const params=new URLSearchParams(new FormData(document.querySelector('#filters')));params.set('page',String(page));const d=await api('?'+params);if(own!==turn)return;newRevision=d.new_revision;centres=d.centres;document.querySelector('#create').hidden=!d.can_prepare;list.replaceChildren();for(const r of d.requests.data){const b=n('button',r.number+' · '+r.status+' · versão '+r.process_version);b.onclick=()=>open(r.id);list.append(b);}last=d.requests.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){const own=++turn;detail.replaceChildren();try{const d=await api('/'+id);if(own!==turn)return;status.textContent='';rememberFocus('requests',{request:id});const r=d.request,panel=n('section');panel.append(n('h2',r.number),n('p',`Estado ${r.status} · versão ${r.process_version} · solicitante #${r.requester_id} · total ${(r.total_minor/100).toFixed(2)} MZN`));for(const item of r.items)panel.append(n('p',`${item.description} · ${item.quantity} ${item.unit} · ${(item.total_minor/100).toFixed(2)} MZN`));for(const step of r.steps)panel.append(n('p',`${step.role} · ${step.decision} · responsável #${step.approver_id??'pendente'} · ${step.comment??''}`));detail.append(panel);if(d.capabilities.save)editor(id,d.revision,r);if(d.capabilities.submit)command(id,d.revision,'submit','Submeter para aprovação',form=>{form.append(n('p','Reserva o montante no livro de Compras e abre as etapas originais de aprovação.'));return ()=>({});});if(d.capabilities.decide)command(id,d.revision,'decide','Decisão independente',form=>{const decision=select(form,'Decisão',[['approved','Aprovar'],['rejected','Rejeitar']]),comment=field(form,'Fundamento individual','','textarea');return ()=>({decision:decision.value,comment:comment.value});});if(r.status==='approved'){const link=n('a','Continuar para propostas e adjudicação');link.href=root.dataset.focusEnabled==='true'?continuationHref('sourcing',{request:id}):'/purchasing/sourcing';if(link.href)detail.append(link);}const history=n('section');detail.append(history);if(d.history_navigation)mountRequestHistory({id,records:d.history,navigation:d.history_navigation,container:history,api,active:()=>own===turn&&history.isConnected,message:text=>status.textContent=text});else{history.append(n('h2','Últimos 25 eventos'));for(const e of d.history){const row=n('details');row.append(n('summary',e.created_at+' · '+e.action+' · autor #'+e.user_id),renderEvidence({anterior:e.before,posterior:e.after}));history.append(row);}}}catch(e){if(own===turn)status.textContent=e.message;}}
|
|
document.querySelector('#filters').onsubmit=e=>{e.preventDefault();page=1;load();};document.querySelector('#create').onclick=()=>{turn++;detail.replaceChildren();editor(0,newRevision);};document.querySelector('#previous').onclick=()=>{if(page>1){page--;load();}};document.querySelector('#next').onclick=()=>{if(page<last){page++;load();}};beginWorkspace({stage:'requests',root,load:load,open:f=>open(f.request),current:()=>turn,error:text=>{status.textContent=text;}});
|