feat: let operators create test orders from approved quotes
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m28s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 47s

Without an approved card payment there was no way to try the board, the
files and the print flow in production. "Criar pedido de teste" on an
approved quote creates the order through the same path as a paid one, marked
TESTE on its card and panel and audited; neither it nor its stage moves
queue anything for Tiny or WhatsApp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-29 14:50:40 -03:00
parent 64c1260a9f
commit 9e69c48d2d
5 changed files with 62 additions and 5 deletions

View File

@@ -87,6 +87,7 @@ main{padding:0 24px 24px}
.card .cust{font-size:13px;font-weight:600;color:var(--tx);width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tags{display:flex;flex-wrap:wrap;gap:6px}
.tag{font-size:12px;padding:3px 8px;border-radius:6px;background:var(--card2);color:var(--tx2)}
.tag-teste{font-size:11px;font-weight:800;letter-spacing:.04em;padding:2px 7px;border-radius:6px;background:var(--warn);color:#1a1300}
.card .foot{display:flex;align-items:center;gap:6px;font-size:12px}
.card .foot .where{margin-left:auto;color:var(--muted)}
.late{font-size:12px;color:#F06A5B;font-weight:600}

View File

@@ -187,11 +187,13 @@ function renderBoard(){
return column;
}));
}
// An operator's test order: no payment, no Tiny, no WhatsApp.
const isTest=order=>order.payment?.provider==='teste';
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'));
top.append(node('span','#'+order.number,'num'),...(isTest(order)?[node('span','TESTE','tag-teste')]:[]),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);
@@ -251,7 +253,8 @@ function renderPanel(order){
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);
row.append(node('span','#'+order.number,'num'),stage,
isTest(order)?node('span','pedido de teste · sem pagamento, não vai ao Tiny nem ao WhatsApp','tag-teste'):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]);
@@ -455,7 +458,17 @@ function review(quote){
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;
card.append(head,content);
if(quote.status==='approved'){
// Tries the whole production flow without a payment.
foot.append(button('Criar pedido de teste',async()=>{
if(!confirm('Criar um pedido de TESTE para esta cotação? Não cobra nada e não vai para o Tiny nem para o WhatsApp.'))return;
const order=await api('/quotes/'+quote.id+'/test-order',{});
say('Pedido de teste #'+order.number+' criado em Arte recebida.');await load();
},'btn ghost'));
card.append(foot);
}
return card;
}
const form=node('form');form.style.display='contents';
const edits=quote.draft.items.map(item=>{const fields={};content.append(quoteItem(item,fields));