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

38 lines
11 KiB
JavaScript

import {rememberFocus} from './purchasing-focus.mjs';
import {evidence as renderEvidence,purchasingStatus} from './purchasing-presenter.mjs';
import {beginWorkspace,focusHref,appendStageLinks} from './purchasing-focus.mjs';
import {openReceiptMovement} from './stock-receipt-focus.mjs';
import {mountSupplierDocuments} from './supplier-documents.mjs';
import {createSupplierPicker as createItemPicker} from './supplier-picker-model.mjs';
const root=document.querySelector('#receipts'),message=document.querySelector('#message'),orders=document.querySelector('#orders'),detail=document.querySelector('#detail');
let page=1,last=1,generation=0,selected=null,preview=null,busy=false;
const node=(tag,text)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;return n;};
const status=text=>{message.textContent=text;};
async function api(path,data){const response=await fetch('/api/purchasing-receipts'+path,{method:data===undefined?'GET':'POST',headers:{Accept:'application/json','X-CSRF-TOKEN':root.dataset.csrf,'X-SGF-Context':root.dataset.context,...(data===undefined?{}:{'Content-Type':'application/json'})},body:data===undefined?undefined:JSON.stringify(data)});const value=await response.json();if(!response.ok)throw Error(value.message||Object.values(value.errors||{}).flat().join(' ')||'Não foi possível concluir.');return value;}
async function list(){const turn=++generation;try{const result=await api('?page='+page);if(turn!==generation)return;orders.replaceChildren();last=result.last_page;for(const order of result.data){const button=node('button',order.number+' · '+order.status);button.onclick=()=>open(order.id);orders.append(button);}document.querySelector('#pagination').textContent=`Página ${page} de ${last}`;document.querySelector('#previous').disabled=page===1;document.querySelector('#next').disabled=page===last;}catch(e){status(e.message);}}
function field(form,label,name,type='text'){const wrap=node('label',label),input=node(type==='textarea'?'textarea':'input');if(type!=='textarea')input.type=type;input.name=name;wrap.append(input);form.append(wrap);return input;}
function select(form,label,name,values){const wrap=node('label',label),input=node('select');input.name=name;input.append(new Option('Seleccione explicitamente',''));for(const [value,text]of values)input.append(new Option(text,String(value)));wrap.append(input);form.append(wrap);return input;}
function stockChoice(parent,order,line,turn,form){
const box=node('fieldset'),legend=node('legend','Artigo de stock · unidade '+line.unit),search=field(box,'Pesquisar código ou nome','stock_search','search'),find=node('button','Pesquisar artigos'),choice=select(box,'Escolha individual','stock_item_id',[]),message=node('p'),previous=node('button','Artigos anteriores'),next=node('button','Artigos seguintes');
search.maxLength=100;message.setAttribute('role','status');for(const button of [find,previous,next])button.type='button';box.prepend(legend);box.append(find,message,previous,next);parent.append(box);box.disabled=true;let current=1,lastPage=1,query='';
const picker=createItemPicker((q,p)=>api('/orders/'+order+'/lines/'+line.id+'/catalogue?'+new URLSearchParams({search:q,page:String(p)})),state=>{
current=state.page;lastPage=state.last;choice.replaceChildren(new Option('Seleccione explicitamente',''));for(const item of state.items)choice.append(new Option(item.code+' · '+item.name+' · '+item.unit,String(item.id)));choice.value=String(state.selected?.id??'');choice.disabled=state.loading||!state.items.length;previous.disabled=state.loading||current<=1;next.disabled=state.loading||current>=lastPage;find.disabled=state.loading;
message.textContent=state.loading?'A pesquisar artigos…':state.error||`Página ${current} de ${lastPage} · ${state.total} artigos compatíveis com a unidade e as ligações da linha. ${state.selected?'Escolhido: '+state.selected.code:'Nenhum artigo escolhido.'}`;form.dispatchEvent(new Event('input',{bubbles:true}));
},()=>turn===generation);
choice.onchange=()=>picker.choose(choice.value);const run=()=>{query=search.value;picker.search(query,1);};find.onclick=run;search.oninput=()=>picker.invalidate();search.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();run();}};previous.onclick=()=>{if(current>1)picker.search(query,current-1);};next.onclick=()=>{if(current<lastPage)picker.search(query,current+1);};picker.activate=enabled=>{box.disabled=!enabled;if(enabled){query=search.value;picker.search(query,1);}else picker.invalidate();};return picker;
}
async function open(id,focusedReceipt=null){const turn=++generation;selected=id;preview=null;detail.replaceChildren();status('A consultar a origem…');try{const data=await api('/orders/'+id);if(turn!==generation)return;rememberFocus('receipts',{order:id,...(focusedReceipt?{receipt:focusedReceipt}:{})});status('');const panel=node('section');panel.append(node('h2',data.order.number),node('p','Estado: '+data.progress.state));const history=node('section');let historySerial=0;
const showReceipt=async(receiptId,pageEpoch=historySerial)=>{try{const result=await api('/history/'+receiptId);if(turn!==generation||pageEpoch!==historySerial)return;if(result.receipt.purchase_order_id!==id)throw Error('A recepção indicada não pertence a esta ordem. Seleccione uma recepção autorizada.');const evidence=node('section');history.append(evidence);evidence.append(node('h3',result.receipt.number),renderEvidence(result));if(result.documents_enabled===true)mountSupplierDocuments({id:receiptId,revision:result.document_revision,canUpload:result.can_attach,root,detail:evidence,receipt:true,basePath:'/api/purchasing-receipts/history/'+receiptId+'/documents',active:()=>turn===generation&&pageEpoch===historySerial&&evidence.isConnected,reload:()=>open(id,receiptId),message:status});}catch(e){if(turn===generation&&pageEpoch===historySerial)status(e.message);}};
const renderHistory=value=>{history.replaceChildren(node('h2','Histórico de recepções'));for(const receipt of value.data){const pageEpoch=historySerial,button=node('button',receipt.number+' · '+receipt.status);button.onclick=()=>showReceipt(receipt.id,pageEpoch);history.append(button);}if(!value.data.length)history.append(node('p','Sem recepções nesta página.'));history.append(node('p','Página '+value.current_page+' de '+value.last_page));for(const [enabled,target,label]of [[value.current_page>1,value.current_page-1,'Anteriores'],[value.current_page<value.last_page,value.current_page+1,'Seguintes']])if(enabled){const b=node('button',label);b.onclick=async()=>{const request=++historySerial;try{const page=await api('/orders/'+id+'/history?page='+target);if(turn===generation&&request===historySerial)renderHistory(page);}catch(e){if(turn===generation&&request===historySerial)status(e.message);}};history.append(b);}};
renderHistory(data.history);detail.append(panel,history);appendStageLinks(root,panel,[{href:focusHref('invoices',{order:id}),label:'Consultar facturas desta ordem'}]);if(focusedReceipt!==null)await showReceipt(focusedReceipt);
if(!data.can_confirm){panel.append(node('p','Este perfil pode consultar. A confirmação exige permissão própria e separação do emissor.'));return;}
const form=node('form'),lines=[];const note=field(form,'Guia de remessa','delivery_note');note.required=true;const date=field(form,'Data efectiva de recepção','received_on','date');date.required=true;const warehouse=select(form,'Armazém de destino','warehouse_id',data.warehouses.map(w=>[w.id,w.code+' · '+w.name]));warehouse.required=true;
for(const line of data.progress.lines){const row=node('article');row.append(node('h3',line.description),node('p',`Encomendado: ${line.ordered_milli}/1000 ${line.unit} · Recebido: ${line.received_milli}/1000 · Aceite: ${line.accepted_milli}/1000 · Pendente de entrega: ${line.delivery_pending_milli}/1000`));const chosen=field(row,'Conferir esta linha','chosen','checkbox'),received=field(row,'Quantidade recebida (até três casas decimais)','received_quantity'),accepted=field(row,'Quantidade aceite','accepted_quantity'),quality=select(row,'Qualidade','quality_status',[['compliant','Conforme'],['reservations','Conforme com reservas'],['nonconforming','Não conforme']]),item=stockChoice(row,id,line,turn,form),notes=field(row,'Evidência de reservas ou divergências','notes','textarea');chosen.onchange=()=>item.activate(chosen.checked);form.append(row);lines.push({line,chosen,received,accepted,quality,item,notes});}
const prepare=node('button','Conferir impacto');prepare.type='submit';const confirm=node('button','Confirmar esta recepção');confirm.type='button';confirm.disabled=true;const impact=node('section');impact.hidden=true;let key=crypto.randomUUID(),draft=0;
form.oninput=()=>{draft++;preview=null;confirm.disabled=true;impact.hidden=true;};form.onsubmit=async event=>{event.preventDefault();if(busy)return;busy=true;prepare.disabled=true;const draftVersion=draft;const payload={revision:data.revision,idempotency_key:key,delivery_note:note.value,received_on:date.value,warehouse_id:Number(warehouse.value),item_revisions:Object.fromEntries(lines.filter(v=>v.chosen.checked&&v.item.selected).map(v=>[v.item.selected.id,v.item.selected.selection_revision])),items:lines.filter(v=>v.chosen.checked).map(v=>({purchase_order_item_id:v.line.id,received_quantity:v.received.value,accepted_quantity:v.accepted.value,quality_status:v.quality.value,stock_item_id:v.item.selected?Number(v.item.selected.id):null,notes:v.notes.value||null}))};try{const result=await api('/orders/'+id+'/preview',payload);if(turn!==generation||draftVersion!==draft)return;preview=result.preview;impact.replaceChildren(node('h3','Impacto antes da confirmação'),renderEvidence({campos_conferidos:result.data,impacto:result.impact}));impact.hidden=false;confirm.disabled=false;status('Confira a guia, as quantidades e o destino. A confirmação volta a validar o workflow.');}catch(e){status(e.message);}finally{busy=false;prepare.disabled=false;}};
confirm.onclick=async()=>{if(!preview||busy)return;busy=true;confirm.disabled=true;try{const result=await api('/orders/'+id+'/confirm',{preview});if(turn!==generation)return;key=crypto.randomUUID();status((result.replayed?'Confirmação já registada: ':'Recepção confirmada: ')+result.receipt.number+' · movimentos físicos: '+result.movements.length);await open(id,result.receipt.id);}catch(e){status(e.message+' Se a resposta se perdeu, repita a mesma confirmação; a chave é preservada e não duplica a recepção.');confirm.disabled=false;}finally{busy=false;}};form.append(prepare,confirm,impact);panel.append(form);
}catch(e){if(turn===generation)status(e.message);}}
document.querySelector('#previous').onclick=()=>{if(page>1){page--;list();}};document.querySelector('#next').onclick=()=>{if(page<last){page++;list();}};beginWorkspace({stage:'receipts',root,load:list,open:f=>f.movement
?openReceiptMovement({movement:f.movement,resolve:id=>api('/movements/'+id+'/origin'),open,current:()=>generation,error:status})
:open(f.order,f.receipt??null),current:()=>generation,error:text=>{status(text);}});