Files
dtf-system/web/site-quality.js
Cauê Faleiros 70a76feb23
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 2m40s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 53s
feat: icons on the buttons, delivery, payment, status and Kanban
- Buttons: add to cart (bag with plus), go to payment (lock), remove from
  the cart (bin).
- Delivery: a pin for pickup in Franca, a truck for home delivery.
- Payment: card and QR code on the methods, copy on the PIX code, a clock on
  its countdown, a check on the confirmation.
- Status: a check when the files are sent, check, alert, redo and cross in
  the quality list instead of Unicode symbols, and a check on finished steps.
- Kanban: a truck or pin for where an order goes, a speech bubble on the
  WhatsApp link, a download arrow on the file buttons.

All Lucide shapes drawn inline, as in the header.

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

203 lines
11 KiB
JavaScript

/* Site DTF — The grade shown to the customer, and the reservations attached to it.
Extracted verbatim from the single inline script in web/index.html.
Loaded as classic scripts in the order listed there: they share one global
scope and run top to bottom, exactly as the original did. */
// ── enviar
// Avalia sozinho conforme o cliente sobe e preenche. Devolve o que ainda falta,
// para a fila dizer em texto em vez de travar num botão.
function avaliar(){
if(ehFolha()){
if(!folhas.length) return {pronto:false, falta:'Arraste sua folha montada para começar.'};
const pdfInvalido=folhas.find(x=>x.measurementError);
if(pdfInvalido) return {pronto:false, falta:pdfInvalido.f.name+': '+pdfInvalido.measurementError};
const larga=folhas.find(x=>x.med && x.med.larg > larguraFilme());
if(larga) return {pronto:false, falta:'A folha '+larga.f.name+' tem '+n1(larga.med.larg)+
' cm e não cabe no filme de '+n1(larguraFilme())+' cm.'};
const semMedida=folhas.filter(x=>!(x.m>0)).length;
if(semMedida) return {pronto:false, falta:'Informe o comprimento de '+semMedida+
' folha'+(semMedida>1?'s':'')+'.'};
metros=folhaTotalM();
reencaixado=false;
const ans=folhas.map(x=>x.an).filter(Boolean);
if(ans.length!==folhas.length){
// A nota precisa cobrir cada folha faturada. Uma folha sem análise não
// herda a nota/desconto de outra folha que o navegador conseguiu abrir.
const ausentes=folhas.filter(f=>!f.an);
const exts=[...new Set(ausentes.map(f=>extDe(f.f.name)))].join(', ');
mostraNota([
['av','Sem nota · '+exts+' sem análise completa',
ausentes.length+' de '+folhas.length+' folha(s) precisam de conferência manual'],
['er','Metro pela tabela · '+rs(TABELA[modo]),
'exportando em PNG ou PDF, cai até '+rs(pisoEscada())],
['fix','Alguém abre seu arquivo','a conferência é feita na mão, na sala']
], 0, folhas.length>1? folhas.length+' folhas' : folhas[0].f.name,
larguraFilme()+' cm · '+fmtM(metros)+' m · preço de tabela');
return {pronto:true};
}
const dpi=Math.min(...ans.map(a=>a.dpi));
const n=Math.max(6,Math.min(100,Math.round(dpi/DPI_AVISA*100)));
const soma=(k)=>ans.reduce((t,a)=>t+a[k],0);
const itens=[
ans.every(a=>a.vetor)
? ['ok','Arte vetorial','sem imagem embutida · imprime em qualquer tamanho']
: [dpi>=DPI_AVISA?'ok':dpi>=DPI_RECUSA?'av':'er','Resolução '+dpi+' DPI',
ans.some(a=>a.fonteDpi==='imagens')
? 'média por área das imagens dentro do PDF · pior em '+
Math.min(...ans.map(a=>a.res? a.res.pior : a.dpi))+' DPI'
: 'resolução do arquivo · arte ampliada antes de exportar não aparece aqui'],
ans.some(a=>a.grande) ? ['ok','Arquivo grande','peças, resíduos e fundo conferidos pela equipe'] : null,
ans.some(a=>a.grande) ? null : ['ok', soma('artes')+' peças na folha',
Math.round(soma('aproveitamento')/ans.length)+'% da folha virou arte'],
ans.some(a=>a.grande) ? null : soma('residuos') ? ['er','Resíduo de recorte',
soma('residuos')+' ponto'+(soma('residuos')>1?'s':'')+' abaixo de 2 mm · a impressora imprime']
: ['ok','Sem resíduo de recorte','nada solto na folha'],
ans.some(a=>a.grande) ? null : soma('encostadas') ? ['fix', soma('encostadas')+' peças a menos de 5 mm',
'se forem artes diferentes, separamos com 5 mm sem custo']
: ['ok','Espaço entre peças','nenhuma abaixo de 6 mm'],
ans.some(a=>a.grande) ? null : ans.some(a=>a.fundoChapado) ? ['er','Fundo chapado','a folha está sem transparência']
: ['ok','Fundo transparente','canal alfa conferido'],
null
].filter(Boolean);
mostraNota(itens, n, folhas.length>1? folhas.length+' folhas' : folhas[0].f.name,
larguraFilme()+' cm · '+fmtM(metros)+' m');
return {pronto:true};
}
if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'};
if(artes.some(a=>a.lendo!=null)) return {pronto:false, falta:'Gerando a prévia das artes grandes…'};
if(artes.some(a=>a.decodeError || !(a.src||a.semPrevia) || !(a.px>0) || !(a.py>0)))
return {pronto:false, falta:'Não conseguimos ler uma das imagens. Exporte-a novamente em PNG ou JPG antes de continuar.'};
if(artes.some(a=>!Number.isFinite(a.cm) || a.cm>larguraFilme()))
return {pronto:false, falta:'A largura da arte precisa caber no filme de '+
n1(larguraFilme())+' cm. Reduza a medida solicitada antes de continuar.'};
const sem=artes.filter(a=>!a.cm).length;
if(sem) return {pronto:false, falta:'Informe a largura de '+sem+' arte'+(sem>1?'s':'')+
' para ver a nota e o preço.'};
// metragem = altura da folha depois de montada, com os 5 mm entre peças
metros=+(montagemCm/100).toFixed(4);
if(!(metros>0)) return {pronto:false, falta:'Montando a folha…'};
const notas=artes.map(a=>Math.max(6,Math.min(100,Math.round(dpiDe(a)/300*100))));
const media=Math.round(notas.reduce((s,n)=>s+n,0)/notas.length), pior=Math.min(...notas);
mostraNota([
['ok','Montagem por nossa conta','5 mm garantidos'],
[cls(pior),'Pior arte em '+pior+'%',pior>=85?'todas em boa resolução':'reenviar maior sai mais barato']
],media,artes.length+' arte'+(artes.length>1?'s':''),
'folha montada por nós · '+fmtM(metros)+' m');
return {pronto:true};
}
// redesenha 220 ms depois da última mexida, para não avaliar a cada tecla
let tAval=null;
function agendaAvaliacao(){
clearTimeout(tAval);
tAval=setTimeout(()=>{
// o produto pode ter sido fechado nesses 220 ms (item foi para o carrinho)
if(!modo) return;
const r=avaliar();
const el=$('aoVivo');
if(!el) return;
if(r.pronto){
el.className='aoVivo pronto';
el.innerHTML='<span class="pt2"></span><span><b>Nota e preço calculados.</b> '+
'Confira abaixo e feche o pedido.</span>';
}else{
el.className='aoVivo';
el.innerHTML='<span class="pt2"></span><span>'+escapeHTML(r.falta)+'</span>';
limpaPaineis(); // o carrinho só some se o pedido estiver vazio
}
},220);
}
function mostraNota(itens,n,titulo,sub){
nota=n;
$('qual').classList.add('on');
$('qNome').textContent=titulo; $('qSub').textContent=sub;
const semNota = n===0;
$('pct').innerHTML = semNota? '—<small>/100</small>' : n+'<small>/100</small>';
const c = semNota? 'var(--fraco)' : n>=85?'var(--verde)':n>=50?'var(--laranja)':'var(--vermelho)';
$('pct').style.color=c;
$('qbi').style.width=(semNota?0:n)+'%'; $('qbi').style.background=c;
$('qitens').innerHTML=itens.map(i=>
'<div class="qi '+i[0]+'"><span class="c">'+
icone(i[0]==='ok'?'ok':i[0]==='av'?'alerta':i[0]==='fix'?'refazer':'fechar')+'</span>'+
'<div><b>'+escapeHTML(i[1])+'</b><span>'+escapeHTML(i[2])+'</span></div></div>').join('');
$('qmsg').style.display='none';
ressalvaResolucao();
carrinho();
}
// A resolução do arquivo não é a das artes dentro dele: um PDF em 300 DPI
// pode ter uma imagem inserida em 180, e é essa que sai serrilhada.
function ressalvaResolucao(){
const box=$('ciente'), chk=$('cienteOk');
let baixas=[], geral=300, recusadas=[];
if(ehFolha()){
// Uma régua só: o número que precifica é o número que bloqueia.
// Recusa quando a média por área fica abaixo de 96 DPI — a folha inteira não
// vale o filme. Se a média passa mas há peças ruins, é ressalva com aceite.
folhas.forEach((x,k)=>{
if(!x.an) return;
const r=x.an.res, cm=x.med? x.med.larg : larguraFilme();
const nome=(folhas.length>1? (k+1)+'. ':'')+x.f.name;
const extra = r ? {pior:r.pior, de:r.total,
quantas:r.quantasBaixas, parte:Math.round(r.fracBaixa*100),
quantasAviso:r.quantasAviso, parteAviso:Math.round(r.fracAviso*100)} : {};
if(x.an.dpi<DPI_RECUSA) recusadas.push(Object.assign({nome, dpi:x.an.dpi, cm}, extra));
else if(x.an.dpi<DPI_AVISA || (r&&r.quantasAviso))
baixas.push(Object.assign({nome, dpi:x.an.dpi, cm}, extra));
});
geral = folhas.some(x=>x.an) ? Math.max(...folhas.filter(x=>x.an).map(x=>x.an.dpi)) : 300;
}else{
const comTamanho=artes.filter(a=>a.cm>0);
recusadas=comTamanho.filter(a=>dpiDe(a)<DPI_RECUSA)
.map(a=>({nome:a.f.name, dpi:Math.round(dpiDe(a)), cm:a.cm}));
baixas=comTamanho.filter(a=>dpiDe(a)>=DPI_RECUSA && dpiDe(a)<DPI_AVISA)
.map(a=>({nome:a.f.name, dpi:Math.round(dpiDe(a)), cm:a.cm}));
const todas=comTamanho.map(a=>dpiDe(a));
geral = todas.length? Math.round(Math.max(...todas)) : 300;
}
// Abaixo de 96 DPI a sala não imprime. Não é desconto, é recusa —
// e não existe aceite que destrave.
if(recusadas.length){
$('cienteTxt').innerHTML=
'<p><b>A média da folha ficou abaixo de '+DPI_RECUSA+' DPI.</b> Nesse ponto a peça '+
'sai serrilhada a ponto de não valer o filme. Reenvie maior ou reduza a largura.</p>'+
'<div class="lista">'+recusadas.map(b=>'<div><b>'+escapeHTML(b.nome)+'</b> — '+
(b.quantas
? 'média de '+b.dpi+' DPI · pior imagem em '+b.pior+' DPI · '+b.quantas+
' de '+b.de+' imagens abaixo de '+DPI_RECUSA+' DPI, cobrindo '+b.parte+'% da arte'
: b.dpi+' DPI em '+n1(b.cm)+' cm · precisa de '+
Math.ceil(DPI_AVISA*b.cm/2.54)+' px de largura')+'</div>')
.join('')+'</div>';
$('cienteTit').textContent='Resolução abaixo do que imprimimos';
box.className='ciente on recusa'; chk.checked=false; $('qOk').disabled=true;
return;
}
$('cienteTit').textContent='Ressalva de resolução';
box.className='ciente';
if(!baixas.length){
box.classList.remove('on'); chk.checked=false; $('qOk').disabled=false; return;
}
const menor=baixas.reduce((p,b)=>b.dpi<p.dpi?b:p);
const media=Math.round(baixas.reduce((s,b)=>s+b.dpi,0)/baixas.length);
$('cienteTxt').innerHTML=
'<p><b>Parte da arte está abaixo de '+DPI_AVISA+' DPI.</b> Essas peças podem sair '+
'com traço serrilhado, e isso não tem conserto depois de impresso. '+
'O resto da folha sai normal.</p>'+
'<div class="lista">'+baixas.map(b=>'<div><b>'+escapeHTML(b.nome)+'</b> — '+
(b.quantasAviso
? 'média de '+b.dpi+' DPI · '+b.quantasAviso+' de '+b.de+' imagens abaixo de '+
DPI_AVISA+' DPI, cobrindo '+b.parteAviso+'% da arte'+
(b.quantas? ' · dessas, '+b.quantas+' abaixo de '+DPI_RECUSA+' DPI ('+
b.parte+'% da arte)' : '')
: b.dpi+' DPI'+(b.cm? ' em '+n1(b.cm)+' cm':''))+
'</div>').join('')+'</div>';
box.className='ciente on'; chk.checked=false; $('qOk').disabled=true;
}
$('cienteOk').addEventListener('change',e=>{
if($('ciente').classList.contains('recusa')) return; // recusa não se aceita
$('qOk').disabled=!e.target.checked;
if(itemAtual && itemAtual.qualityStatus==='warning'){
itemAtual.qualityAcknowledged=e.target.checked;
pintaEntrega();
}
});