Files
dtf-system/web/portal.js
Cauê Faleiros 223854e392
All checks were successful
Build and deploy / Validate source (push) Successful in 4s
Build and deploy / Integration suite on a real stack (push) Successful in 2m37s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 59s
feat: the order page in the Site's look, and no search box
"Meus pedidos" was still the prototype's plain page. It now has the Site's
stripe, header and footer, and its type, colours and cards: each order shows
its status, a progress line through production (or the correction the team
asked for), the items with metres and price, where it goes, and on request
the history, the files to download and the correction form. Carts waiting
for payment are listed with a button to pay. Sign-in and account creation
are side by side.

The header, stripe, menu, footer and tokens moved from index.html's inline
style to site-shell.css, which both pages load. The header's search box,
which did nothing, is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:13:35 -03:00

184 lines
12 KiB
JavaScript

/* Meus pedidos: the customer's orders, their progress and files, sign-in and
the artwork correction the team may ask for. */
const $=id=>document.getElementById(id);
const node=(tag,text,className)=>{const el=document.createElement(tag);if(text!==undefined&&text!==null)el.textContent=text;if(className)el.className=className;return el;};
const money=c=>(c/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
const quando=d=>new Date(d).toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const dia=d=>new Date(d).toLocaleDateString('pt-BR');
const metros=m=>Number(m).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2})+' m';
// Lucide shapes (ISC licence), the same as on the Site.
const ICONES={
baixar:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
local:'<path d="M20 10c0 4.99-5.54 10.19-7.4 11.8a1 1 0 0 1-1.2 0C9.54 20.19 4 14.99 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
caminhao:'<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/>',
ok:'<path d="M20 6 9 17l-5-5"/>',
alerta:'<path d="M12 7v6"/><path d="M12 17h.01"/>',
relogio:'<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
};
function icone(nome){
const span=document.createElement('span');span.className='icw';
span.innerHTML='<svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false">'+ICONES[nome]+'</svg>';
return span.firstChild;
}
const PRODUTO={file:'DTF Têxtil · folha montada',avulsa:'DTF Têxtil · artes avulsas',
uvfile:'DTF UV · folha montada',uv:'DTF UV · artes avulsas'};
// The order's way through production, as the customer sees it.
const TRILHA=['rec','tra','fil','imp','fin'];
let scope,states={},environment='';
async function api(path,body){const r=await fetch('/api'+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 informados.');error.status=r.status;throw error;}return d;}
const aviso=text=>{$('message').textContent=text||'';};
function button(text,fn,className='bSec'){const b=node('button',text,className);b.type='button';b.onclick=async()=>{b.disabled=true;try{await fn();}catch(e){aviso(e.message);}finally{b.disabled=false;}};return b;}
function forgetCheckout(){for(const k of ['dtf-quote','dtf-request-key','dtf-request-body'])localStorage.removeItem(k);}
function trilha(state){
const ol=node('ol',null,'trilha');
const atual=state==='cor'?-1:TRILHA.indexOf(state);
TRILHA.forEach((s,i)=>{
const li=node('li',null,i<atual||state==='fin'?'feito':i===atual?'atual':'');
const marca=node('span',null,'marca');
if(i<atual||state==='fin')marca.append(icone('ok'));else marca.textContent=String(i+1);
li.append(marca,node('b',states[s]||s));
ol.append(li);
});
return ol;
}
function entrega(snapshot){
const d=snapshot.destination, p=node('p',null,'entregaPed');
p.append(icone(d?'caminhao':'local'),node('span',d?'Entrega pela Jadlog · '+d.city+'/'+d.state:'Retirada em Franca · Av. Major Elias Motta, 1281'));
return p;
}
function itens(snapshot){
const ul=node('ul',null,'itensPed');
for(const item of snapshot.items){
const li=node('li');
li.append(node('span',PRODUTO[item.mode]||item.mode,'nome'),node('span',metros(item.billed_metres),'qt'),node('b',money(item.total_cents)));
ul.append(li);
}
const f=snapshot.freight;
if(f&&f.total_cents){const li=node('li',null,'frete');li.append(node('span','Frete','nome'),node('span',f.days?'até '+f.days+' dias úteis':'','qt'),node('b',money(f.total_cents)));ul.append(li);}
return ul;
}
async function load(){
try{
const session=await api('/session');scope=session.cart_scope;environment=session.environment;
$('dev-notice').hidden=environment!=='local';
const account=await api('/account/me');
$('auth').hidden=!!account.customer;$('logout').hidden=!account.customer;
$('account').textContent=account.customer?'Conta '+account.customer.mail:'';
$('guest').textContent=account.customer?'':'Você está vendo os pedidos feitos neste navegador. Entre ou crie uma conta para ver seus pedidos em qualquer lugar.';
const data=await api('/customer/orders');states=data.states;
const quotes=$('quotes');quotes.replaceChildren();
if(data.quotes.length){
quotes.append(node('h2','Aguardando pagamento','tituloGrupo'));
for(const quote of data.quotes){
const card=node('article',null,'pedido pendente');
const topo=node('div',null,'pedTopo');
const nome=node('div');nome.append(node('h3','Carrinho de '+dia(quote.created_at)),node('span',quote.approved?'Pronto para pagar':'Em conferência pela equipe','data'));
topo.append(nome);
if(quote.approved)topo.append(node('b',money(quote.approved.total_cents),'valor'));
card.append(topo);
if(quote.approved){const link=node('a','Ir para o pagamento','bPri');link.href='/pagamento?quote='+quote.id;card.append(link);}
else card.append(node('p','A equipe confere a metragem e a nota da arte. Você recebe o valor para pagar em seguida.','nota2'));
quotes.append(card);
}
}
const orders=$('orders');orders.replaceChildren();
orders.append(node('h2','Pedidos','tituloGrupo'));
if(!data.orders.length){
const vazio=node('div',null,'vazio');
vazio.append(node('p','Nenhum pedido pago ainda.'));
const link=node('a','Enviar uma arte','bPri');link.href='/#envio';vazio.append(link);
orders.append(vazio);
}
const aberto=new URLSearchParams(location.search).get('order');
for(const order of data.orders){
const card=node('article',null,'pedido'+(order.state==='cor'?' correcao':''));card.id='order-'+order.id;
const topo=node('div',null,'pedTopo');
const nome=node('div');nome.append(node('h3','Pedido #'+order.number),node('span',quando(order.created_at),'data'));
topo.append(nome,node('span',states[order.state],'estado est-'+order.state),node('b',money(order.snapshot.total_cents)+(environment==='local'?' · teste':''),'valor'));
card.append(topo);
if(order.state==='cor'){
const p=node('p',null,'pedAlerta');p.append(icone('alerta'),node('span','A equipe precisa de uma correção na sua arte. Abra os detalhes para ver o motivo e enviar o arquivo corrigido.'));
card.append(p);
}else card.append(trilha(order.state));
card.append(itens(order.snapshot),entrega(order.snapshot));
const content=node('div',null,'detalhe');
const ver=button('Ver detalhes e arquivos',async()=>{
if(content.childElementCount){content.replaceChildren();ver.textContent='Ver detalhes e arquivos';return;}
await details(order.id,content,order.created_at);ver.textContent='Fechar detalhes';
},'bLink');
card.append(ver,content);orders.append(card);
if(aberto===order.id){await details(order.id,content,order.created_at);ver.textContent='Fechar detalhes';card.scrollIntoView({block:'start'});}
}
}catch(error){aviso(error.message);}
}
async function details(id,container,criado){
const order=await api('/customer/orders/'+id);container.replaceChildren();
const hist=node('div',null,'secao');hist.append(node('h4','Andamento'));
const ol=node('ol',null,'historico');
// Payment is where the order starts: the first step is not a movement.
if(criado){const li=node('li');li.append(node('b',states.rec||'Arte recebida'),node('span',quando(criado)));ol.append(li);}
for(const h of order.history){
const li=node('li');li.append(node('b',states[h.to_state]),node('span',quando(h.created_at)));
if(h.reason)li.append(node('p',h.reason,'motivo'));
ol.append(li);
}
hist.append(ol);container.append(hist);
if(order.files.length){
const arq=node('div',null,'secao');arq.append(node('h4','Arquivos'));
for(const file of order.files){
const row=node('div',null,'arquivo');
const info=node('div');
info.append(node('b',file.name),node('span',(file.kind==='final'?'Arquivo final':'Correção')+' · item '+(file.item_index+1)+' · '+
(file.expired?'expirado':!file.active?'substituído':'disponível até '+dia(file.expires_at))));
row.append(info);
if(file.active&&!file.expired){
const b=button('Baixar',async()=>{const d=await api('/customer/orders/'+id+'/files/'+file.id+'/download');const link=node('a');link.href=d.url;link.download=d.name;link.referrerPolicy='no-referrer';link.click();});
b.prepend(icone('baixar'));row.append(b);
}
arq.append(row);
}
container.append(arq);
}
if(order.state==='cor'){
const form=node('form',null,'secao correcaoForm');form.append(node('h4','Enviar arte corrigida'));
const inputs=order.snapshot.items.map((item,index)=>{
const label=node('label',(index+1)+'. '+(PRODUTO[item.mode]||item.mode));
const input=node('input');input.type='file';input.multiple=true;input.dataset.item=index;
form.append(label,input);return input;
});
const nl=node('label','O que foi corrigido');const note=node('textarea');note.required=true;note.maxLength=1000;note.rows=3;
form.append(nl,note);
const submit=node('button','Enviar correção','bPri');submit.type='submit';form.append(submit);
form.onsubmit=async event=>{event.preventDefault();submit.disabled=true;try{
const files=[];for(const [index,input] of inputs.entries())for(const file of input.files)files.push({item_index:index,upload_id:await dtfUpload(file,{api,scope:scope+':correction:'+id+':'+order.version,progress:text=>aviso(text)})});
if(!files.length)throw new Error('Selecione pelo menos um arquivo corrigido.');
await api('/customer/orders/'+id+'/corrections',{version:order.version,files,note:note.value});
aviso('Correção enviada. Aguarde a conferência da equipe.');await details(id,container,criado);
}catch(error){aviso(error.message);}finally{submit.disabled=false;}};
container.append(form);
}
}
async function migrateGuestCart(previous,next){
if(previous===next)return;
const database=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onupgradeneeded=()=>r.result.createObjectStore('cart');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});
await new Promise((resolve,reject)=>{const tx=database.transaction('cart','readwrite'),store=tx.objectStore('cart'),req=store.get(previous);req.onsuccess=()=>{if(req.result){store.put(req.result,next);store.delete(previous);}};tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);});
database.close();
}
async function authenticate(path,body){
const previous=scope,guest=!(await api('/account/me')).customer;
await api(path,body);forgetCheckout();aviso('Acesso confirmado.');
const next=(await api('/session')).cart_scope;
if(guest)try{await migrateGuestCart(previous,next);}catch{aviso('Acesso confirmado. Não foi possível transferir o carrinho salvo neste navegador.');}
await load();$('password').value='';$('register-password').value='';
}
$('login').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/login',{email:$('email').value,password:$('password').value});}catch(e){aviso(e.message);}};
$('register').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/register',{customer:{cnpj:$('cnpj').value,zap:$('phone').value,mail:$('register-email').value},password:$('register-password').value});}catch(e){aviso(e.message);}};
$('logout').onclick=async()=>{try{await window.dtfForgetPrivateData();await api('/account/logout',{});location.reload();}catch(error){aviso('Não foi possível limpar os dados deste navegador. Feche as abas do Site e limpe os dados do site nas configurações do navegador.');}};
$('refresh').onclick=load;
load();