All checks were successful
Build and deploy / Validate source (push) Successful in 8s
Build and deploy / Integration suite on a real stack (push) Successful in 2m22s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m34s
Every quote waited for an operator before it could be paid, so an order placed at night waited for the morning. A cart the Site priced is now approved when the quote is created, through the same server pricing the operator's approval uses (app/quote_review.py). Orders above QUOTE_AUTO_MAX_METRES (50 m) and items claiming a discount on art the Site could not analyse still wait for review; the Kanban shows which quotes were approved automatically and why the others wait. The grade is still computed in the browser (roadmap 3.2, 3.9), so the discount remains a customer-supplied value until the server computes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
674 lines
45 KiB
JavaScript
674 lines
45 KiB
JavaScript
/* Kanban DTF: production board, quote review, payments and integrations.
|
||
Everything is built with textContent; nothing from an order or a customer is
|
||
ever parsed as HTML. The previous screen is in git tag ui-v1. */
|
||
const $ = id=>document.getElementById(id);
|
||
// Remove credentials saved by older local builds. Only HttpOnly sessions now.
|
||
sessionStorage.removeItem('dtf-operator');
|
||
|
||
const STAGE_COLORS={rec:'var(--rec)',tra:'var(--tra)',fil:'var(--fil)',imp:'var(--imp)',cor:'var(--cor)',fin:'var(--fin)'};
|
||
const FLOW=['rec','tra','fil','imp','fin'];
|
||
const PRODUCT={file:'DTF Têxtil · folha montada',avulsa:'DTF Têxtil · artes avulsas',uvfile:'DTF UV · folha montada',uv:'DTF UV · artes avulsas'};
|
||
const SHORT={file:'Têxtil folha',avulsa:'Têxtil avulsa',uvfile:'UV folha',uv:'UV avulsa'};
|
||
const EVENTS={payment_approved:'Pagamento aprovado',production_started:'Produção iniciada',correction_needed:'Correção necessária',ready:'Pedido pronto'};
|
||
const LATE_HOURS=4;
|
||
// Payment outcomes are recorded by the API in English; operators read Portuguese.
|
||
function outcomeText(outcome){
|
||
const rules=[
|
||
[/^refused: paid (\S+) but quote total is (\d+)/,m=>'valor pago ('+(m[1]==='None'?'não informado':money(Number(m[1])))+') diferente do total da cotação ('+money(Number(m[2]))+')'],
|
||
[/^refused: quote expired/,()=>'a cotação venceu antes do pagamento'],
|
||
[/^refused: quote not found/,()=>'cotação não encontrada'],
|
||
[/^refused: reference is not a quote id/,()=>'a referência do pagamento não é uma cotação'],
|
||
[/^refused: quote was never reviewed/,()=>'a cotação ainda não tinha sido revisada'],
|
||
[/^refused: quote uses an obsolete/,()=>'a cotação usa uma montagem antiga'],
|
||
[/^attention: payment (\w+) for order (\d+)/,m=>'pagamento '+(m[1]==='refunded'?'estornado':'cancelado')+' depois do pedido #'+m[2]],
|
||
];
|
||
for(const [pattern,text] of rules){const m=outcome.match(pattern);if(m)return text(m);}
|
||
return outcome;
|
||
}
|
||
|
||
let board=null;
|
||
let tab='board', filter='all', search='', openOrder=null, openToken=0;
|
||
let quoteKind='pending', pickedQuote=null;
|
||
let olderFinished=[], finishedMore=null;
|
||
|
||
const money=cents=>(cents/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
|
||
const metres=value=>Number(value).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2})+' m';
|
||
const when=value=>new Date(value).toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||
function ago(value){
|
||
const minutes=Math.max(0,Math.round((Date.now()-new Date(value))/60000));
|
||
if(minutes<60)return 'há '+minutes+' min';
|
||
const hours=Math.round(minutes/60);
|
||
return hours<48?'há '+hours+' h':'há '+Math.round(hours/24)+' dias';
|
||
}
|
||
function node(tag,text,className){const e=document.createElement(tag);if(text!==undefined&&text!==null)e.textContent=text;if(className)e.className=className;return e;}
|
||
function button(text,fn,className='btn'){
|
||
const b=node('button',text,className);b.type='button';
|
||
b.onclick=async event=>{event.stopPropagation();b.disabled=true;try{await fn();}catch(e){say(e.message,true);}finally{b.disabled=false;}};
|
||
return b;
|
||
}
|
||
// Messages are notifications, not state: they clear themselves. Errors stay
|
||
// longer so they can be read; a caller can hold one for as long as it needs.
|
||
let sayTimer=null;
|
||
function say(text,error=false,ms){
|
||
clearTimeout(sayTimer);
|
||
for(const id of ['status','login-status']){$(id).textContent=text||'';$(id).classList.toggle('error',!!error);}
|
||
if(text)sayTimer=setTimeout(()=>say(''),ms??(error?8000:4000));
|
||
}
|
||
function icon(path){
|
||
const ns='http://www.w3.org/2000/svg';const svg=document.createElementNS(ns,'svg');
|
||
svg.setAttribute('width','14');svg.setAttribute('height','14');svg.setAttribute('viewBox','0 0 24 24');
|
||
svg.setAttribute('fill','none');svg.setAttribute('stroke','currentColor');svg.setAttribute('stroke-width','2.2');svg.setAttribute('aria-hidden','true');
|
||
for(const d of path){const p=document.createElementNS(ns,d.startsWith('circle')?'circle':'path');
|
||
if(d.startsWith('circle')){const [cx,cy,r]=d.split(' ').slice(1);p.setAttribute('cx',cx);p.setAttribute('cy',cy);p.setAttribute('r',r);}else p.setAttribute('d',d);svg.append(p);}
|
||
return svg;
|
||
}
|
||
const ICON_OK=['M5 12l4 4 10-10'], ICON_WARN=['M12 8v5M12 17h.01','circle 12 12 9'];
|
||
async function api(path,body){
|
||
const r=await fetch('/api/operator'+path,{headers:{'Content-Type':'application/json'},...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)})});
|
||
const d=await r.json();if(!r.ok){const error=new Error(typeof d.detail==='string'?d.detail:'Confira os campos ('+r.status+').');error.status=r.status;throw error;}return d;
|
||
}
|
||
function download(uid){return async()=>{
|
||
const result=await api('/uploads/'+uid+'/download');
|
||
const link=node('a');link.href=result.url;link.referrerPolicy='no-referrer';link.download=result.name;link.click();
|
||
};}
|
||
function manifest(production){
|
||
const url=URL.createObjectURL(new Blob([JSON.stringify(production,null,2)],{type:'application/json'}));
|
||
const link=node('a');link.href=url;link.download='dtf-layout-'+production.sources[0].upload_id.slice(0,8)+'.json';
|
||
link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||
}
|
||
|
||
// ---- Loading and tabs ------------------------------------------------------
|
||
|
||
async function load(){
|
||
try{
|
||
board=await api('/board');
|
||
const known=new Set(board.orders.map(o=>o.id));
|
||
board.orders.push(...olderFinished.filter(o=>!known.has(o.id)));
|
||
if(finishedMore===null)finishedMore=board.finished_total>board.finished_shown;
|
||
$('login').hidden=true;$('app').hidden=false;
|
||
say('Atualizado às '+new Date().toLocaleTimeString('pt-BR')+tinyNotice,false,tinyNotice?6000:2500);
|
||
render();
|
||
}catch(e){
|
||
if(e.status===401||e.status===503){$('app').hidden=true;$('login').hidden=false;$('drawer').hidden=true;}
|
||
// Not being signed in yet is the normal start, not an error.
|
||
if(e.status===401){say('');return;}
|
||
say(e.message,true);
|
||
}
|
||
}
|
||
function showTab(name){
|
||
tab=name;
|
||
for(const b of document.querySelectorAll('[data-tab]'))b.setAttribute('aria-selected',String(b.dataset.tab===name));
|
||
for(const id of ['board','quotes','payments','integrations'])$('tab-'+id).hidden=name!==id;
|
||
const lists={quotes:loadQuotes,payments:loadIssues,integrations:loadEvents};
|
||
if(board&&lists[name])lists[name]().catch(e=>say(e.message,true));
|
||
}
|
||
function render(){
|
||
$('who').textContent=board.operator||'';
|
||
$('count-board').textContent=String(board.orders.filter(o=>o.state!=='fin').length);
|
||
$('badge-quotes').textContent=board.pending_total?String(board.pending_total):'';
|
||
$('badge-payments').textContent=board.payment_issues_total?String(board.payment_issues_total):'';
|
||
tinyHeader();renderBoard();renderIntegrations();
|
||
// Lists live on their own pages of the API; refresh the one being looked at.
|
||
const lists={quotes:loadQuotes,payments:loadIssues,integrations:loadEvents};
|
||
if(lists[tab])lists[tab]().catch(e=>say(e.message,true));
|
||
if(openOrder){const order=board.orders.find(o=>o.id===openOrder);if(order)renderPanel(order);else closePanel();}
|
||
}
|
||
|
||
// ---- Production board ----------------------------------------------------
|
||
|
||
function printState(order){
|
||
if(order.state==='fil'||order.state==='imp')return {text:'Final aprovado',tone:'ok'};
|
||
if(order.state==='fin')return {text:'Concluído',tone:'ok'};
|
||
if(order.state==='cor')return {text:'Aguardando o cliente',tone:''};
|
||
const rows=order.print_files||[], items=order.snapshot.items.length;
|
||
const ready=rows.filter(r=>r.status==='ready').length;
|
||
if(rows.some(r=>r.status==='pending'||r.status==='rendering'))return {text:'Gerando arquivo…',tone:''};
|
||
if(ready===items)return {text:'PDF pronto',tone:'ok'};
|
||
if(rows.some(r=>r.status==='manual'||r.status==='failed'))
|
||
return {text:items>1&&ready?ready+' de '+items+' pronto':'Preparar à mão',tone:'warn'};
|
||
return {text:'Sem arquivo gerado',tone:'warn'};
|
||
}
|
||
function matches(order){
|
||
const modes=order.snapshot.items.map(i=>i.mode);
|
||
if(filter==='textil'&&!modes.some(m=>m==='file'||m==='avulsa'))return false;
|
||
if(filter==='uv'&&!modes.some(m=>m==='uvfile'||m==='uv'))return false;
|
||
if(filter==='manual'&&!(order.print_files||[]).some(r=>r.status==='manual'||r.status==='failed'))return false;
|
||
if(!search)return true;
|
||
const c=order.snapshot.customer, d=order.snapshot.destination||{};
|
||
const hay=[String(order.number),c.mail,c.cnpj,d.recipient,d.city].join(' ').toLowerCase();
|
||
const digits=search.replace(/\D/g,'');
|
||
return hay.includes(search)||(digits.length>=3&&hay.replace(/\D/g,'').includes(digits));
|
||
}
|
||
function renderBoard(){
|
||
const filters=[['all','Todos'],['textil','Têxtil'],['uv','UV'],['manual','Preparar à mão']];
|
||
$('filters').replaceChildren(...filters.map(([key,label])=>{
|
||
const b=node('button',label,'chip');b.type='button';b.setAttribute('aria-pressed',String(filter===key));
|
||
b.onclick=()=>{filter=key;renderBoard();};return b;}));
|
||
const active=board.orders.filter(o=>['rec','tra','fil','imp'].includes(o.state));
|
||
const queued=active.reduce((s,o)=>s+o.snapshot.items.reduce((t,i)=>t+Number(i.billed_metres),0),0);
|
||
$('board-summary').textContent=metres(queued)+' em produção';
|
||
$('kan').replaceChildren(...Object.entries(board.states).map(([state,title])=>{
|
||
const column=node('section',undefined,'col');column.dataset.state=state;
|
||
const orders=board.orders.filter(o=>o.state===state&&matches(o));
|
||
const total=orders.reduce((s,o)=>s+o.snapshot.items.reduce((t,i)=>t+Number(i.billed_metres),0),0);
|
||
const head=node('div',undefined,'col-head');const line=node('div');
|
||
const swatch=node('span',undefined,'swatch');swatch.style.setProperty('--c',STAGE_COLORS[state]);
|
||
// Finished orders are a recent window, not the whole history: say so.
|
||
const count=state==='fin'&&board.finished_total>orders.length?orders.length+' de '+board.finished_total:String(orders.length);
|
||
line.append(swatch,node('h2',title),node('span',count,'n'));
|
||
head.append(line,node('small',orders.length?metres(total)+' no total':'vazio'));
|
||
column.append(head);
|
||
for(const order of orders)column.append(card(order));
|
||
if(state==='fin'&&finishedMore&&!search&&filter==='all')moreButton(column,true,loadOlderFinished);
|
||
// Dragging only goes forward to the next stage. Going back or asking for a
|
||
// correction needs a reason, so those happen in the order panel.
|
||
column.ondragover=e=>{if(!column.classList.contains('allowed'))return;e.preventDefault();column.classList.add('target');};
|
||
column.ondragleave=()=>column.classList.remove('target');
|
||
column.ondrop=async e=>{e.preventDefault();column.classList.remove('target');
|
||
const order=board.orders.find(o=>o.id===e.dataTransfer.getData('text/plain'));
|
||
if(order&&order.state!==state)try{await move(order,state);}catch(error){say(error.message,true);}};
|
||
return column;
|
||
}));
|
||
}
|
||
function card(order){
|
||
const c=node('button',undefined,'card');c.type='button';c.dataset.card=order.id;c.draggable=true;
|
||
if(order.id===openOrder)c.classList.add('open');
|
||
const top=node('div',undefined,'row');
|
||
top.append(node('span','#'+order.number,'num'),node('span',ago(order.updated_at),'age'));
|
||
const tags=node('div',undefined,'tags');
|
||
for(const item of order.snapshot.items)tags.append(node('span',SHORT[item.mode]+' · '+metres(item.billed_metres),'tag'));
|
||
const foot=node('div',undefined,'foot');const p=printState(order);
|
||
const status=node('span',p.text,p.tone);status.style.display='flex';status.style.alignItems='center';status.style.gap='5px';
|
||
if(p.tone==='ok')status.prepend(icon(ICON_OK));if(p.tone==='warn')status.prepend(icon(ICON_WARN));
|
||
const dest=order.snapshot.destination;
|
||
foot.append(status,node('span',dest?'Entrega · '+dest.state:'Retirada','where'));
|
||
c.append(top,node('div',order.snapshot.customer.mail,'cust'),tags,foot);
|
||
if(['rec','tra','fil','imp'].includes(order.state)&&Date.now()-new Date(order.updated_at)>LATE_HOURS*3600000)
|
||
c.append(node('div','Parado há mais de '+LATE_HOURS+' h','late'));
|
||
c.onclick=()=>openPanel(order.id);
|
||
c.ondragstart=e=>{
|
||
e.dataTransfer.setData('text/plain',order.id);
|
||
const forward=board.transitions[order.state].filter(s=>s!=='cor');
|
||
for(const col of document.querySelectorAll('.col[data-state]'))col.classList.toggle('allowed',forward.includes(col.dataset.state));
|
||
say(forward.length?'Mover para '+forward.map(s=>board.states[s]).join(' ou '):'Pedido finalizado',false,60000);
|
||
};
|
||
c.ondragend=()=>{for(const col of document.querySelectorAll('.col'))col.classList.remove('allowed','target');say('');};
|
||
return c;
|
||
}
|
||
async function loadOlderFinished(){
|
||
const shown=board.orders.filter(o=>o.state==='fin').sort((a,b)=>a.created_at<b.created_at?-1:1);
|
||
const oldest=shown[0];const params=new URLSearchParams({limit:'50'});
|
||
if(oldest){params.set('before_created_at',oldest.created_at);params.set('before_id',oldest.id);}
|
||
const page=await api('/orders/finished?'+params);
|
||
const known=new Set(board.orders.map(o=>o.id));
|
||
const fresh=page.orders.filter(o=>!known.has(o.id));
|
||
olderFinished.push(...fresh);board.orders.push(...fresh);finishedMore=page.has_more;renderBoard();
|
||
}
|
||
async function move(order,state,reason=''){
|
||
await api('/orders/'+order.id+'/move',{state,version:order.version,reason});
|
||
await load();
|
||
}
|
||
|
||
// ---- Order panel ---------------------------------------------------------
|
||
|
||
function openPanel(id){openOrder=id;const order=board.orders.find(o=>o.id===id);$('drawer').hidden=false;renderPanel(order);renderBoard();}
|
||
function closePanel(){openOrder=null;$('drawer').hidden=true;$('panel').replaceChildren();if(board)renderBoard();}
|
||
$('scrim').onclick=closePanel;
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&openOrder)closePanel();});
|
||
|
||
function layoutPreview(production){
|
||
const ns='http://www.w3.org/2000/svg', w=Number(production.film_width_cm), h=Math.max(1,Number(production.height_cm));
|
||
const svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 '+w+' '+h);svg.setAttribute('role','img');
|
||
svg.setAttribute('aria-label','Montagem: '+production.placements.length+' peças em '+w+' × '+h+' cm');
|
||
const film=document.createElementNS(ns,'rect');film.setAttribute('width',w);film.setAttribute('height',h);film.setAttribute('fill','#0F1113');svg.append(film);
|
||
const colors=['#2BB6D9','#E0508F','#E9C23A','#6FD19C','#FF8B2B','#A78BFA'];
|
||
for(const p of production.placements){const r=document.createElementNS(ns,'rect');
|
||
r.setAttribute('x',p.x_cm);r.setAttribute('y',p.y_cm);r.setAttribute('width',p.width_cm);r.setAttribute('height',p.length_cm);
|
||
r.setAttribute('fill',colors[p.source_index%colors.length]);r.setAttribute('fill-opacity','.55');svg.append(r);}
|
||
return svg;
|
||
}
|
||
function renderPanel(order){
|
||
const token=++openToken, panel=$('panel');panel.dataset.order=order.id;
|
||
const color=STAGE_COLORS[order.state];
|
||
const head=node('div',undefined,'panel-head');
|
||
const row=node('div',undefined,'row');
|
||
const stage=node('span',board.states[order.state],'stage');stage.style.setProperty('--c',color);
|
||
const close=node('button',undefined,'close');close.type='button';close.setAttribute('aria-label','Fechar');close.append(icon(['M6 6l12 12M18 6L6 18']));close.onclick=closePanel;
|
||
row.append(node('span','#'+order.number,'num'),stage,node('span','pago '+when(order.created_at),'faint'),close);
|
||
const steps=node('ol',undefined,'steps');steps.setAttribute('aria-label','Etapas');
|
||
const at=FLOW.indexOf(order.state);
|
||
for(const [i,s] of FLOW.entries()){const li=node('li',board.states[s]);li.style.setProperty('--c',STAGE_COLORS[s]);
|
||
if(at>=0&&i<at)li.classList.add('done');if(i===at)li.classList.add('now');steps.append(li);}
|
||
head.append(row,steps,moves(order));
|
||
|
||
const body=node('div',undefined,'panel-body');
|
||
const s=order.snapshot, dest=s.destination;
|
||
const facts=node('section',undefined,'facts');
|
||
const who=node('div');who.append(node('p','Cliente','eyebrow'),node('b',s.customer.mail),node('span','CNPJ '+s.customer.cnpj+' · '+s.customer.zap));
|
||
const where=node('div');where.append(node('p','Entrega','eyebrow'),node('b',dest?dest.recipient:'Retirada em Franca'),
|
||
node('span',dest?dest.street+', '+dest.number+(dest.complement?' '+dest.complement:'')+' · '+dest.district+' · '+dest.city+'/'+dest.state+' · CEP '+dest.postal_code:'Cliente retira na fábrica'));
|
||
facts.append(who,where);
|
||
const items=node('section');items.append(node('p','Itens e arquivos de impressão','eyebrow'));
|
||
const itemsList=node('div');itemsList.style.display='flex';itemsList.style.flexDirection='column';itemsList.style.gap='10px';
|
||
s.items.forEach((item,index)=>itemsList.append(itemCard(order,item,index)));
|
||
items.append(itemsList);
|
||
const total=node('p',undefined,'muted');total.append('Total pago: ',node('b',money(s.total_cents)));items.append(total);
|
||
const finals=node('section',undefined,'finals');finals.append(node('p','Arquivos finais','eyebrow'),node('p','Carregando…','faint'));
|
||
const history=node('section');history.append(node('p','Histórico','eyebrow'),node('p','Carregando…','faint'));
|
||
body.append(facts,items,finals,history);
|
||
panel.replaceChildren(head,body);
|
||
|
||
api('/orders/'+order.id+'/files').then(rows=>{if(token===openToken)finalsSection(order,rows,finals);}).catch(e=>say(e.message,true));
|
||
api('/orders/'+order.id+'/history').then(rows=>{if(token===openToken)historySection(order,rows,history);}).catch(e=>say(e.message,true));
|
||
}
|
||
function moves(order){
|
||
const box=node('div');box.style.display='flex';box.style.flexDirection='column';box.style.gap='10px';
|
||
const row=node('div',undefined,'moves');
|
||
const reason=node('form',undefined,'reason');reason.hidden=true;
|
||
for(const next of board.transitions[order.state]){
|
||
if(next==='cor'){
|
||
const b=node('button','Pedir correção','btn danger');b.type='button';b.dataset.move='cor';
|
||
b.onclick=()=>{reason.hidden=!reason.hidden;backForm.hidden=true;if(!reason.hidden)reason.querySelector('textarea').focus();};row.append(b);
|
||
}else{
|
||
const b=button('Mover para '+board.states[next],()=>move(order,next),'btn primary');b.dataset.move=next;row.append(b);
|
||
}
|
||
}
|
||
// Undo a mistaken move: one stage back, internal reason, no customer message.
|
||
const previous=board.back?.[order.state];
|
||
const backForm=node('form',undefined,'reason');backForm.hidden=true;
|
||
if(previous){
|
||
const b=node('button','Voltar para '+board.states[previous],'btn ghost');b.type='button';b.dataset.back=previous;
|
||
b.onclick=()=>{backForm.hidden=!backForm.hidden;reason.hidden=true;if(!backForm.hidden)backForm.querySelector('input').focus();};
|
||
row.append(b);
|
||
const why=node('input');why.placeholder='Motivo';why.setAttribute('aria-label','Motivo do retorno');why.required=true;why.maxLength=1000;
|
||
const go=node('button','Voltar para '+board.states[previous],'btn');
|
||
backForm.append(why,go);
|
||
backForm.onsubmit=async e=>{e.preventDefault();go.disabled=true;try{await move(order,previous,why.value.trim());}catch(error){say(error.message,true);go.disabled=false;}};
|
||
}
|
||
if(!row.children.length)row.append(node('span','Pedido concluído.','muted'));
|
||
const text=node('textarea');text.placeholder='Motivo da correção';text.setAttribute('aria-label','Motivo da correção');text.maxLength=1000;text.required=true;
|
||
const confirm=node('button','Enviar para correção','btn danger');
|
||
reason.append(text,confirm);
|
||
reason.onsubmit=async e=>{e.preventDefault();confirm.disabled=true;try{await move(order,'cor',text.value.trim());}catch(error){say(error.message,true);confirm.disabled=false;}};
|
||
box.append(row,reason,backForm);return box;
|
||
}
|
||
const PRINT_TEXT={pending:'Na fila para gerar',rendering:'Gerando arquivo…',ready:'PDF gerado',manual:'Preparar à mão',failed:'Falhou ao gerar'};
|
||
function itemCard(order,item,index){
|
||
const row=(order.print_files||[]).find(r=>r.item_index===index);
|
||
const box=node('div',undefined,'item');
|
||
if(row&&(row.status==='manual'||row.status==='failed'))box.classList.add('attention');
|
||
const preview=node('div',undefined,'preview');
|
||
if(item.production?.placements)preview.append(layoutPreview(item.production));
|
||
const body=node('div',undefined,'body');
|
||
const title=node('div',undefined,'title');title.append(node('b',PRODUCT[item.mode]),node('span',metres(item.billed_metres),'mono'));
|
||
const spec=item.production;
|
||
const line=spec?(spec.placements.length+' peças em '+spec.film_width_cm+' × '+spec.height_cm+' cm · nota '+item.grade):('nota '+item.grade);
|
||
body.append(title,node('div',line,'line'));
|
||
const file=node('div',undefined,'file');
|
||
if(row){
|
||
const tone=row.status==='ready'?'ok':row.status==='manual'||row.status==='failed'?'warn':'muted';
|
||
const label=node('span',PRINT_TEXT[row.status]+(row.status==='ready'&&row.name?' · '+row.name:'')+(row.status==='manual'&&row.detail.reason?': '+row.detail.reason:''),tone);
|
||
file.append(tone==='ok'?icon(ICON_OK):tone==='warn'?icon(ICON_WARN):'',label);
|
||
if(row.status==='ready'&&row.detail){body.append(file,node('div',[row.detail.film_width_cm+' × '+row.detail.height_cm+' cm',
|
||
row.detail.min_dpi?'menor resolução '+row.detail.min_dpi+' DPI':'',row.detail.vector_sources?'PDF vetorial':''].filter(Boolean).join(' · '),'line'));}
|
||
else body.append(file);
|
||
}else body.append(node('div','Arquivo de impressão ainda não gerado','line'));
|
||
const buttons=node('div',undefined,'buttons');
|
||
if(row?.status==='ready')buttons.append(button('Baixar PDF',download(row.upload_id)));
|
||
item.uploads.forEach((uid,i)=>buttons.append(button(item.uploads.length>1?'Original '+(i+1):'Baixar original',download(uid),'btn ghost')));
|
||
if(spec?.placements)buttons.append(button('Manifesto',()=>manifest(spec),'btn ghost'));
|
||
if(['rec','tra'].includes(order.state)&&(!row||row.status==='manual'||row.status==='failed'))
|
||
buttons.append(button('Gerar novamente',async()=>{await api('/orders/'+order.id+'/print-files',{});await load();},'btn ghost'));
|
||
body.append(buttons);
|
||
box.append(preview,body);return box;
|
||
}
|
||
function finalsSection(order,revisions,section){
|
||
section.replaceChildren(node('p','Arquivos finais','eyebrow'));
|
||
for(const file of revisions){
|
||
const rev=node('div',undefined,'rev');
|
||
rev.append(node('b',(file.kind==='final'?'Final':'Correção do cliente')+' · item '+(file.item_index+1)+' · '+file.name),
|
||
node('span',(file.expired?'expirado':file.active?'atual':'substituído')+' · '+when(file.created_at)+' · '+file.note,'faint'));
|
||
if(!file.expired){const b=button('Baixar',download(file.upload_id),'btn ghost');b.style.alignSelf='flex-start';rev.append(b);}
|
||
section.append(rev);
|
||
}
|
||
if(!['rec','tra','cor'].includes(order.state)){if(!revisions.length)section.append(node('p','Nenhum arquivo final registrado.','faint'));return;}
|
||
// A generated print file reproduces the approved layout: offer it first. Not
|
||
// after a customer correction, which replaced the artwork it was made from.
|
||
const corrected=revisions.some(file=>file.kind==='correction');
|
||
const form=node('form');
|
||
const inputs=order.snapshot.items.map((item,index)=>{
|
||
const slot=node('label',undefined,'slot');slot.append('Item '+(index+1)+' · '+PRODUCT[item.mode]);
|
||
const input=node('input');input.type='file';input.multiple=true;input.required=true;input.dataset.finalItem=index;
|
||
const generated=corrected?null:(order.print_files||[]).find(r=>r.item_index===index&&r.status==='ready');
|
||
if(generated){
|
||
const use=node('input');use.type='checkbox';use.checked=true;use.dataset.useGenerated=index;input.required=false;input.hidden=true;
|
||
use.onchange=()=>{input.hidden=use.checked;input.required=!use.checked;};
|
||
const choice=node('label',undefined,'check');choice.append(use,'Usar o PDF gerado ('+generated.name+')');
|
||
slot.append(choice);input.generated=()=>use.checked?generated.upload_id:null;
|
||
}
|
||
slot.append(input);form.append(slot);return input;
|
||
});
|
||
const note=node('input');note.placeholder='Nota da revisão';note.required=true;note.maxLength=1000;
|
||
const check=node('input');check.type='checkbox';check.required=true;check.dataset.confirm='';
|
||
const confirm=node('label',undefined,'check');confirm.append(check,'Arquivos conferidos');
|
||
const submit=node('button','Aprovar arquivos finais','btn primary');
|
||
form.append(note,confirm,submit);
|
||
form.onsubmit=async event=>{event.preventDefault();submit.disabled=true;try{
|
||
const refs=[];
|
||
for(const [index,input] of inputs.entries()){
|
||
const generated=input.generated?.();
|
||
if(generated){refs.push({item_index:index,upload_id:generated});continue;}
|
||
for(const file of input.files){
|
||
const uid=await dtfUpload(file,{api,startPath:'/orders/'+order.id+'/uploads',scope:'operator:'+order.id+':'+order.version,progress:text=>say(text)});
|
||
refs.push({item_index:index,upload_id:uid});
|
||
}
|
||
}
|
||
await api('/orders/'+order.id+'/final-files',{version:order.version,files:refs,note:note.value});
|
||
await load();
|
||
}catch(error){say(error.message,true);submit.disabled=false;}};
|
||
section.append(form);
|
||
}
|
||
function historySection(order,rows,section){
|
||
const list=node('ol',undefined,'timeline');
|
||
for(const r of [...rows].reverse()){
|
||
const li=node('li');li.style.setProperty('--c',STAGE_COLORS[r.to_state]);
|
||
const text=node('div');text.append(node('b',r.back?'Voltou para '+board.states[r.to_state]:board.states[r.from_state]+' → '+board.states[r.to_state]));
|
||
if(r.reason)text.append(node('span',' · '+r.reason,'muted'));
|
||
text.append(node('small',when(r.created_at)+' · '+r.operator));li.append(text);list.append(li);
|
||
}
|
||
const first=node('li');first.style.setProperty('--c','var(--rec)');
|
||
const t=node('div');t.append(node('b','Pedido recebido e pago'),node('small',when(order.created_at)+' · '+(board.environment==='local'&&order.payment?.provider==='fake'?'pagamento de teste':'Site')));
|
||
first.append(t);list.append(first);
|
||
section.replaceChildren(node('p','Histórico','eyebrow'),list);
|
||
}
|
||
|
||
// ---- Quotes --------------------------------------------------------------
|
||
|
||
function quoteMetres(q){return (q.approved||q.draft).items.reduce((s,i)=>s+Number(i.billed_metres||i.metres),0);}
|
||
let quoteRows=[], quoteTotal=0, quotePage=1, quoteSize=20, currentQuote=null, quoteLoading=0;
|
||
async function loadQuotes(){
|
||
const token=++quoteLoading;
|
||
const page=await api('/quotes?'+new URLSearchParams({kind:quoteKind,limit:String(quoteSize),offset:String((quotePage-1)*quoteSize)}));
|
||
if(token!==quoteLoading)return;
|
||
quoteRows=page.quotes;quoteTotal=page.total;
|
||
if(quotePage>1&&!quoteRows.length&"eTotal){quotePage=Math.ceil(quoteTotal/quoteSize);return loadQuotes();}
|
||
renderQuotes();
|
||
}
|
||
function renderQuotes(){
|
||
const kinds=[['pending','A revisar',board.pending_total],['approved','Aprovadas, aguardando pagamento',board.approved_total]];
|
||
$('quote-filters').replaceChildren(...kinds.map(([key,label,count])=>{
|
||
const b=node('button',label+' · '+count,'chip');b.type='button';b.setAttribute('aria-pressed',String(quoteKind===key));
|
||
b.onclick=()=>{quoteKind=key;quotePage=1;currentQuote=null;loadQuotes().catch(e=>say(e.message,true));};return b;}));
|
||
if(!currentQuote||(!quoteRows.some(q=>q.id===currentQuote.id)&¤tQuote.status==='pending_review'))currentQuote=quoteRows[0]||null;
|
||
else currentQuote=quoteRows.find(q=>q.id===currentQuote.id)||currentQuote;
|
||
const list=quoteRows.map(q=>{
|
||
const b=node('button',undefined,'qitem');b.type='button';b.dataset.quotePick=q.id;b.setAttribute('aria-current',String(q.id===currentQuote?.id));
|
||
const left=node('div');left.append(node('b',q.draft.customer.mail),
|
||
node('span',q.draft.items.map(i=>SHORT[i.mode]).join(' + ')+' · '+metres(quoteMetres(q))));
|
||
if(q.draft.items.some(i=>i.production?.version!==2))left.append(node('span','Montagem antiga: peça nova cotação','flag'));
|
||
else if(q.draft.items.some(i=>i.quality_status==='warning'))left.append(node('span','Ressalva de resolução aceita pelo cliente','flag'));
|
||
if(q.auto_approved)left.append(node('span','Aprovada automaticamente'));
|
||
else if(q.review_reason)left.append(node('span','Revisão manual: '+q.review_reason,'flag'));
|
||
const right=node('div',undefined,'right');
|
||
right.append(node('b',q.approved?money(q.approved.total_cents):q.id.slice(0,8),'mono'),node('span',ago(q.created_at)));
|
||
b.append(left,right);b.onclick=()=>{currentQuote=q;renderQuotes();};return b;
|
||
});
|
||
if(!list.length)list.push(node('p',quoteKind==='pending'?'Nenhuma cotação para revisar.':'Nenhuma cotação aprovada aguardando pagamento.','faint'));
|
||
if(quoteTotal>quoteSize||quotePage>1)list.push(pager({page:quotePage,size:quoteSize,total:quoteTotal,noun:'cotações',compact:true,
|
||
onPage:n=>{quotePage=n;loadQuotes().catch(e=>say(e.message,true));},
|
||
onSize:n=>{quoteSize=n;quotePage=1;loadQuotes().catch(e=>say(e.message,true));}}));
|
||
$('quote-list').replaceChildren(...list);
|
||
$('reviews').replaceChildren(currentQuote?review(currentQuote):node('p','Selecione uma cotação.','note'));
|
||
}
|
||
function review(quote){
|
||
const card=node('article',undefined,'review');card.style.display='contents';card.dataset.quote=quote.id;
|
||
const head=node('div',undefined,'head');const title=node('div');
|
||
title.append(node('b',quote.draft.customer.mail),node('div','Cotação '+quote.id.slice(0,8)+' · enviada '+ago(quote.created_at)+' · '+
|
||
(quote.draft.destination?'entrega em '+quote.draft.destination.city+'/'+quote.draft.destination.state:'retirada em Franca'),'muted'));
|
||
head.append(title);
|
||
const content=node('div',undefined,'content');
|
||
const foot=node('div',undefined,'foot');
|
||
if(quote.status==='pending_review'&"e.draft.items.some(item=>item.production?.version!==2)){
|
||
content.append(node('p','Montagem antiga. Solicite uma nova cotação ao cliente.','muted'));
|
||
card.append(head,content);return card;
|
||
}
|
||
if(quote.status!=='pending_review'){
|
||
content.append(node('p',quote.status==='expired'?'Cotação expirada. O cliente precisa solicitar outra.':
|
||
'Aprovada: '+money(quote.approved.total_cents)+' · aguardando pagamento','muted'));
|
||
for(const item of quote.draft.items)content.append(quoteItem(item,null));
|
||
card.append(head,content);return card;
|
||
}
|
||
const form=node('form');form.style.display='contents';
|
||
const edits=quote.draft.items.map(item=>{const fields={};content.append(quoteItem(item,fields));
|
||
return ()=>({...item,metres:fields.metres.value,grade:Number(fields.grade.value)});});
|
||
const check=node('input');check.type='checkbox';check.required=true;
|
||
const confirm=node('label',undefined,'check');confirm.append(check,'Arquivos, metragem e nota conferidos');
|
||
const submit=node('button','Aprovar cotação','btn primary');
|
||
foot.append(confirm,submit);
|
||
form.append(content,foot);
|
||
form.onsubmit=async e=>{e.preventDefault();submit.disabled=true;
|
||
try{const approved=await api('/quotes/'+quote.id+'/approve',{items:edits.map(fn=>fn())});
|
||
// Keep showing the quote just approved while its list refreshes.
|
||
currentQuote={...quote,approved,status:'approved'};say('Cotação aprovada.');await load();}
|
||
catch(error){say(error.message,true);submit.disabled=false;}};
|
||
card.append(head,form);return card;
|
||
}
|
||
function quoteItem(item,fields){
|
||
const box=node('div',undefined,'item');
|
||
const preview=node('div',undefined,'preview');preview.style.width='160px';
|
||
if(item.production?.placements)preview.append(layoutPreview(item.production));
|
||
const body=node('div',undefined,'body');
|
||
const title=node('div',undefined,'title');
|
||
const quality=item.quality_status==='ok'?'qualidade ok':item.quality_status==='warning'?'ressalva '+(item.quality_acknowledged?'aceita':'não aceita'):'qualidade não verificada';
|
||
title.append(node('b',PRODUCT[item.mode]),node('span',quality+' · nota '+item.grade,item.quality_status==='ok'?'ok':'warn'));
|
||
body.append(title);
|
||
if(item.production)body.append(node('div',item.production.sources.length+' arquivo(s) · '+item.production.placements.length+' peças · '+
|
||
item.production.film_width_cm+' × '+item.production.height_cm+' cm','line'));
|
||
if(fields){
|
||
const grid=node('div',undefined,'fields');
|
||
const asked=node('label','Metros do cliente');const a=node('input');a.value=item.metres;a.readOnly=true;asked.append(a);
|
||
const m=node('label','Metros conferidos');fields.metres=node('input');Object.assign(fields.metres,{type:'number',min:'0.001',max:'12000',step:'any',value:item.metres,required:true});m.append(fields.metres);
|
||
const g=node('label','Nota conferida');fields.grade=node('input');Object.assign(fields.grade,{type:'number',min:'0',max:'100',step:'1',value:item.grade,required:true});g.append(fields.grade);
|
||
grid.append(asked,m,g);body.append(grid);
|
||
}
|
||
const buttons=node('div',undefined,'buttons');
|
||
item.uploads.forEach((uid,i)=>buttons.append(button(item.uploads.length>1?'Original '+(i+1):'Baixar original',download(uid),'btn ghost')));
|
||
if(item.production?.placements)buttons.append(button('Manifesto',()=>manifest(item.production),'btn ghost'));
|
||
body.append(buttons);box.append(preview,body);return box;
|
||
}
|
||
|
||
// ---- Payments and integrations ------------------------------------------
|
||
|
||
const tinyResult=new URLSearchParams(location.search).get('tiny');
|
||
const tinyNotice=tinyResult?(tinyResult==='connected'?' · Tiny conectado.':' · Não foi possível conectar o Tiny. Tente de novo.'):'';
|
||
if(tinyResult)history.replaceState(null,'',location.pathname);
|
||
function tinyHeader(){
|
||
const t=board.tiny||{};const el=$('tiny');
|
||
if(!t.configured){el.textContent='';return;}
|
||
const trouble=!t.connected||t.problem;
|
||
el.textContent=!t.connected?'Tiny não conectado':t.problem?'Tiny: verificar conexão':'Tiny conectado';
|
||
el.style.setProperty('--dot',trouble?'var(--warn)':'var(--fin)');
|
||
}
|
||
const TINY_PROBLEMS={refused:'O Tiny recusou a renovação da conexão. Conecte de novo.',
|
||
expired:'A conexão com o Tiny expirou. Conecte de novo.',
|
||
'renewal-failing':'A última renovação falhou; o sistema tenta de novo sozinho.',
|
||
expiring:'A conexão expira em breve e não está sendo renovada.'};
|
||
function tinyDetail(t){
|
||
if(!t.connected_by)return 'Conecte com uma conta do Tiny.';
|
||
const parts=[t.connected_by+' · conectado em '+when(t.connected_at),'renovado em '+when(t.renewed_at),
|
||
t.offline?'sem expiração diária':t.expires_at?'válido até '+when(t.expires_at):'',
|
||
'envio de pedidos '+(t.orders_enabled?'ativo':'desativado')].filter(Boolean);
|
||
const problem=TINY_PROBLEMS[t.problem];
|
||
return (problem?problem+(t.failed_at&&t.problem!=='expired'?' (falha em '+when(t.failed_at)+')':'')+' · ':'')+parts.join(' · ');
|
||
}
|
||
function integration(name,state,tone,text,actions=[]){
|
||
const box=node('div',undefined,'integration');
|
||
const head=node('div',undefined,'head');const dot=node('span',undefined,'dot');
|
||
const colors={ok:['var(--fin)','var(--ok)'],warn:['var(--warn)','var(--warn)'],off:['#5B6168','var(--muted)']}[tone];
|
||
dot.style.setProperty('--dot',colors[0]);const em=node('em',state);em.style.setProperty('--dotx',colors[1]);
|
||
head.append(dot,node('b',name),em);box.append(head,node('p',text));
|
||
if(actions.length){const b=node('div',undefined,'buttons');b.append(...actions);box.append(b);}
|
||
return box;
|
||
}
|
||
// The page bar under every list: page size, range, first/previous/page/next/last.
|
||
const PAGE_SIZES=[10,20,50,100];
|
||
function pager({page,size,total,noun,onPage,onSize,compact=false}){
|
||
const pages=Math.max(1,Math.ceil(total/size));
|
||
const bar=node('div',undefined,'pager'+(compact?' compact':''));
|
||
const sizeLabel=node('label',undefined,'pager-size');
|
||
const select=node('select');select.setAttribute('aria-label','Itens por página');
|
||
for(const n of PAGE_SIZES){const o=node('option',String(n));o.value=String(n);o.selected=n===size;select.append(o);}
|
||
select.onchange=()=>onSize(Number(select.value));
|
||
sizeLabel.append('Mostrar',select,'por página');
|
||
const nav=node('div',undefined,'pager-nav');
|
||
const from=total?(page-1)*size+1:0, to=Math.min(total,page*size);
|
||
const go=(label,aria,target,disabled)=>{const b=node('button',label,'pg');b.type='button';b.setAttribute('aria-label',aria);b.disabled=disabled;b.onclick=()=>onPage(target);return b;};
|
||
const pageLabel=node('label',undefined,'pager-page');
|
||
const input=node('input');input.type='number';input.min='1';input.max=String(pages);input.value=String(page);input.setAttribute('aria-label','Página');
|
||
input.onchange=()=>onPage(Math.min(pages,Math.max(1,Math.round(Number(input.value))||1)));
|
||
pageLabel.append(compact?'':'Página',input,'de '+pages);
|
||
nav.append(go('«','Primeira página',1,page<=1),go('‹','Página anterior',page-1,page<=1),pageLabel,
|
||
go('›','Próxima página',page+1,page>=pages),go('»','Última página',pages,page>=pages));
|
||
bar.append(sizeLabel,node('span',(compact?'':'Mostrando ')+from+' a '+to+' de '+total+(compact?'':' '+noun),'pager-range'),nav);
|
||
return bar;
|
||
}
|
||
function chips(container,options,current,pick){
|
||
container.replaceChildren(...options.map(([key,label])=>{
|
||
const b=node('button',label,'chip');b.type='button';b.setAttribute('aria-pressed',String(current===key));
|
||
b.onclick=()=>pick(key);return b;}));
|
||
}
|
||
function moreButton(container,has,fn){
|
||
if(!has)return;
|
||
const b=button('Carregar mais',fn,'btn ghost');b.style.alignSelf='flex-start';b.style.marginTop='8px';container.append(b);
|
||
}
|
||
|
||
// Payments needing a person: paged, open or resolved (history).
|
||
let issueState='open', issueRows=[], issueTotal=0, issuePage=1, issueSize=20, issueLoading=0;
|
||
async function loadIssues(){
|
||
const token=++issueLoading;
|
||
const page=await api('/payment-events?'+new URLSearchParams({state:issueState,limit:String(issueSize),offset:String((issuePage-1)*issueSize)}));
|
||
if(token!==issueLoading)return;
|
||
issueRows=page.issues;issueTotal=page.total;
|
||
if(issuePage>1&&!issueRows.length&&issueTotal){issuePage=Math.ceil(issueTotal/issueSize);return loadIssues();}
|
||
renderIssues();
|
||
}
|
||
function renderIssues(){
|
||
const filters=node('div',undefined,'chips');filters.style.marginBottom='12px';
|
||
chips(filters,[['open','Abertos'],['resolved','Resolvidos'],['all','Todos']],issueState,key=>{issueState=key;issuePage=1;loadIssues().catch(e=>say(e.message,true));});
|
||
const list=node('div');
|
||
if(!issueRows.length)list.append(node('p',issueState==='resolved'?'Nenhum pagamento resolvido.':'Nenhum pagamento pendente.','faint'));
|
||
for(const issue of issueRows){
|
||
const box=node('div',undefined,'issue');
|
||
if(issue.resolved_at)box.classList.add('done');
|
||
box.append(issue.resolved_at?icon(ICON_OK):icon(ICON_WARN));box.firstChild.style.color=issue.resolved_at?'var(--ok)':'var(--warn)';
|
||
const text=node('div',undefined,'text');
|
||
text.append(node('b',(issue.amount_cents==null?'Pagamento sem valor informado':'Pagamento de '+money(issue.amount_cents))+': '+outcomeText(issue.outcome)),
|
||
node('span',issue.provider+' · evento '+issue.event_id+' · cotação '+(issue.reference||'—')+' · '+when(issue.received_at)));
|
||
if(issue.resolved_at){
|
||
text.append(node('span','Resolvido por '+issue.resolved_by+' em '+when(issue.resolved_at)+': '+issue.resolution));
|
||
box.append(text);
|
||
}else{
|
||
const form=node('form');const note=node('input');note.placeholder='Resolução';note.setAttribute('aria-label','Resolução');note.required=true;note.minLength=3;note.maxLength=1000;
|
||
const save=node('button','Registrar resolução','btn warn');form.append(note,save);
|
||
form.onsubmit=async e=>{e.preventDefault();save.disabled=true;
|
||
try{await api('/payment-events/'+issue.id+'/resolve',{note:note.value});say('Resolução registrada.');await load();}
|
||
catch(error){say(error.message,true);save.disabled=false;}};
|
||
box.append(text,form);
|
||
}
|
||
list.append(box);
|
||
}
|
||
const bar=pager({page:issuePage,size:issueSize,total:issueTotal,noun:'pagamentos',
|
||
onPage:n=>{issuePage=n;loadIssues().catch(e=>say(e.message,true));},
|
||
onSize:n=>{issueSize=n;issuePage=1;loadIssues().catch(e=>say(e.message,true));}});
|
||
$('payments').replaceChildren(filters,list,bar);
|
||
}
|
||
|
||
// The integration send log: filtered and paged on the server.
|
||
let eventFilter={provider:'',status:'',event:'',order:''}, eventRows=[], eventTotal=0, eventPage=1, eventSize=20, eventLoading=0, eventTimer=null;
|
||
async function loadEvents(){
|
||
const token=++eventLoading;
|
||
const params=new URLSearchParams({limit:String(eventSize),offset:String((eventPage-1)*eventSize)});
|
||
for(const [key,value] of Object.entries(eventFilter))if(value)params.set(key,value);
|
||
const page=await api('/events?'+params);
|
||
if(token!==eventLoading)return;
|
||
eventRows=page.events;eventTotal=page.total;
|
||
if(eventPage>1&&!eventRows.length&&eventTotal){eventPage=Math.ceil(eventTotal/eventSize);return loadEvents();}
|
||
renderEvents();
|
||
}
|
||
function eventFilters(){
|
||
const bar=$('event-filters');if(bar.dataset.ready)return;bar.dataset.ready='1';
|
||
const select=(label,key,options)=>{
|
||
const field=node('label',undefined,'field');field.append(label);
|
||
const s=node('select');for(const [value,text] of options){const o=node('option',text);o.value=value;s.append(o);}
|
||
s.onchange=()=>{eventFilter[key]=s.value;eventPage=1;loadEvents().catch(e=>say(e.message,true));};
|
||
field.append(s);return field;
|
||
};
|
||
const order=node('label',undefined,'field');order.append('Pedido');
|
||
const input=node('input');input.type='number';input.min='1';input.placeholder='Nº';input.setAttribute('inputmode','numeric');
|
||
input.oninput=()=>{clearTimeout(eventTimer);eventTimer=setTimeout(()=>{eventFilter.order=input.value.trim();eventPage=1;loadEvents().catch(e=>say(e.message,true));},350);};
|
||
order.append(input);
|
||
bar.append(
|
||
select('Destino','provider',[['','Todos'],['tiny','Tiny'],['whatsapp','WhatsApp']]),
|
||
select('Situação','status',[['','Todas'],['delivered','Enviados'],['queued','Na fila'],['failing','Com erro']]),
|
||
select('Evento','event',[['','Todos'],...Object.entries(EVENTS)]),
|
||
order);
|
||
}
|
||
function eventResult(e){
|
||
const receipt=e.receipt||{};
|
||
if(e.delivered_at){
|
||
const situacao=receipt.situacao?' · '+receipt.situacao:'';
|
||
if(receipt.status==='created')return ['Criado no Tiny nº '+receipt.tiny_number+situacao,'good'];
|
||
if(receipt.status==='already-created')return ['Já existia no Tiny nº '+receipt.tiny_number+' (reenvio)'+situacao,'good'];
|
||
if(receipt.status==='status-updated')return ['Tiny nº '+receipt.tiny_number+situacao,'good'];
|
||
if(receipt.status==='status-unchanged')return ['Tiny nº '+receipt.tiny_number+' já estava'+situacao.replace(' · ',' em '),''];
|
||
if(receipt.status==='not-applicable')return ['Não se aplica ao Tiny',''];
|
||
return ['Registrado, sem envio',''];
|
||
}
|
||
if(e.last_error)return ['Nova tentativa · '+e.last_error+' (tentativa '+e.attempts+')','retry'];
|
||
return ['Na fila',''];
|
||
}
|
||
function renderEvents(){
|
||
const rows=eventRows.map(e=>{
|
||
const tr=node('tr');const [result,cls]=eventResult(e);
|
||
tr.append(node('td',when(e.delivered_at||e.available_at),'muted'),node('td','#'+e.payload.number,'mono'),
|
||
node('td',{tiny:'Tiny',whatsapp:'WhatsApp'}[e.provider]||e.provider),
|
||
node('td',EVENTS[e.payload.event]||e.payload.event),node('td',result,cls));
|
||
return tr;
|
||
});
|
||
if(!rows.length){const tr=node('tr');const td=node('td','Nenhum envio encontrado.','faint');td.colSpan=5;tr.append(td);rows.push(tr);}
|
||
$('events').replaceChildren(...rows);
|
||
$('events-more').replaceChildren(pager({page:eventPage,size:eventSize,total:eventTotal,noun:'envios',
|
||
onPage:n=>{eventPage=n;loadEvents().catch(e=>say(e.message,true));},
|
||
onSize:n=>{eventSize=n;eventPage=1;loadEvents().catch(e=>say(e.message,true));}}));
|
||
}
|
||
|
||
function renderIntegrations(){
|
||
const t=board.tiny||{};const cards=[];
|
||
if(!t.configured)cards.push(integration('Tiny','Não configurado','off','Credenciais ausentes no servidor.'));
|
||
else{
|
||
const connect=button(t.connected?'Reconectar':'Conectar Tiny',async()=>{const r=await api('/tiny/connect',{});location.href=r.url;},t.connected?'btn ghost':'btn');
|
||
const actions=[connect];
|
||
if(t.connected){
|
||
actions.unshift(button('Testar conexão',async()=>{const r=await api('/tiny/test',{});
|
||
say(r.ok?'Conexão com o Tiny ok: pedidos, contatos, os 4 produtos e a retirada conferidos.':'Tiny: '+Object.entries(r.results).filter(([,v])=>v!=='ok').map(([k,v])=>k+': '+v).join(' · '),!r.ok);}));}
|
||
cards.push(integration('Tiny',!t.connected?'Não conectado':t.problem?'Verificar':'Conectado',
|
||
t.connected&&!t.problem?'ok':'warn',tinyDetail(t),actions));
|
||
}
|
||
const mp=board.providers?.payment;
|
||
cards.push(integration('Mercado Pago',mp==='mercadopago'?'Ativo':'Não configurado',mp==='mercadopago'?'ok':'off',
|
||
mp==='mercadopago'?'PIX e cartão ativos.':'Aguardando credenciais.'));
|
||
cards.push(integration('Frete','Não configurado','off','Somente retirada.'));
|
||
cards.push(integration('WhatsApp','Não configurado','off','Mensagens registradas, sem envio.'));
|
||
$('integrations').replaceChildren(...cards);
|
||
eventFilters();
|
||
}
|
||
|
||
// ---- Wiring --------------------------------------------------------------
|
||
|
||
for(const b of document.querySelectorAll('[data-tab]'))b.onclick=()=>showTab(b.dataset.tab);
|
||
$('search').oninput=e=>{search=e.target.value.trim().toLowerCase();renderBoard();if(search)showTab('board');};
|
||
$('login').onsubmit=async e=>{e.preventDefault();try{await api('/login',{email:$('email').value,password:$('password').value});await load();}catch(error){say(error.message,true);}finally{$('password').value='';}};
|
||
$('logout').onclick=async()=>{await api('/logout',{});for(const key of Object.keys(localStorage))if(key.startsWith('dtf-'))localStorage.removeItem(key);location.reload();};
|
||
$('refresh').onclick=load;
|
||
load();
|