Files
dtf-system/web/site-config.js
Cauê Faleiros f3fc733352
All checks were successful
Build and deploy / Integration suite on a real stack (push) Successful in 2m37s
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 56s
fix: the upload-time notice leaves with the files, and says it in one line
The notice stayed on the product page after the item went to the cart,
showing the last file's size on the next order. It is now redrawn wherever
the files are cleared and hidden when no product is open. The text is one
line: "Envio: até 8 min (501,2 MB), conforme sua internet. Mantenha a
página aberta."

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

155 lines
10 KiB
JavaScript

/* Site DTF — Product tables, commercial ladders, shared state and field validation.
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. */
const $=id=>document.getElementById(id);
const rs=v=>'R$ '+v.toFixed(2).replace('.',',');
// Ícones (Lucide, licença ISC) desenhados no próprio HTML: nada a carregar, e a
// cor vem do texto em volta. Os que ficam no HTML estático são os mesmos.
const ICONES={
busca:'<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
conta:'<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
sacola:'<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/>',
enviar:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
girar:'<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>',
espelhar:'<path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/>',
fechar:'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
adicionar:'<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M12 10v6"/><path d="M9 13h6"/>',
cadeado:'<path d="M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
lixeira:'<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2"/><path d="M10 11v6"/><path d="M14 11v6"/>',
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"/>',
cartao:'<path d="M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z"/><path d="M2 10h20"/>',
qrcode:'<path d="M3 3h5v5H3z"/><path d="M16 3h5v5h-5z"/><path d="M3 16h5v5H3z"/><path d="M21 16h-3a2 2 0 0 0-2 2v3"/><path d="M21 21v.01"/><path d="M12 7v3a2 2 0 0 1-2 2H7"/><path d="M3 12h.01"/><path d="M12 3h.01"/><path d="M12 16v.01"/><path d="M16 12h1"/><path d="M21 12v.01"/><path d="M12 21v-1"/>',
copiar:'<path d="M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2Z"/><path d="M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"/>',
relogio:'<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
ok:'<path d="M20 6 9 17l-5-5"/>',
okCirculo:'<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
alerta:'<path d="M12 7v6"/><path d="M12 17h.01"/>',
refazer:'<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>',
};
const icone=n=>'<svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false">'+ICONES[n]+'</svg>';
const fmt=b=>b<1048576?(b/1024).toFixed(0)+' KB'
:b<1073741824?(b/1048576).toFixed(1).replace('.',',')+' MB':(b/1073741824).toFixed(1).replace('.',',')+' GB';
// ── modos
const MODOS={
file: {tit:'Arquivo por metro', lg:'DTF têxtil · 57 cm', larg:57, multi:true, folha:true,
zt:'Arraste sua folha montada aqui', zs:'PNG, JPG ou PDF · 57 cm de largura'},
avulsa:{tit:'Artes avulsas', lg:'DTF têxtil · 57 cm', larg:57, multi:true,
zt:'Arraste suas artes aqui',
zs:'PNG ou JPG com fundo transparente · informe a largura de cada uma'},
uvfile:{tit:'Arquivo por metro · UV', lg:'Adesivo UV · 28,5 cm', larg:28.5, multi:true, folha:true,
zt:'Arraste sua folha de UV aqui', zs:'PNG, JPG ou PDF · 28,5 cm de largura'},
uv: {tit:'Artes avulsas · UV', lg:'Adesivo UV · 28,5 cm', larg:28.5, multi:true,
zt:'Arraste suas artes de UV',
zs:'PNG ou JPG com fundo transparente · informe a largura de cada arte'}
};
// Quatro escadas. A de artes avulsas e a da folha pronta mais a montagem:
// R$ 10,00 no têxtil, R$ 14,00 no UV, constante em todas as faixas.
const FAIXAS={
file: [[90,14.90],[75,16.20],[60,17.50],[40,18.70],[0,19.90]],
avulsa:[[90,24.90],[75,26.20],[60,27.50],[40,28.70],[0,29.90]],
uvfile:[[90,69.90],[75,73.90],[60,77.90],[40,81.90],[0,85.90]],
uv: [[90,83.90],[75,87.90],[60,91.90],[40,95.90],[0,99.90]]
};
const TABELA={file:19.90, avulsa:29.90, uvfile:85.90, uv:99.90}; // teto de cada escada
const MONTAGEM={avulsa:10.00, uv:14.00}; // o que a montagem soma sobre a folha pronta
const ehFolha=m=>!!MODOS[m||modo].folha;
const pisoEscada=m=>FAIXAS[m||modo][0][1];
const MINIMO_M=1, GAP_CM=0.5, RODAPE_CM=5;
const RENDERIZA=/[.](png|jpe?g|webp|gif|bmp)$/i;
// Folha pronta: dá para medir PNG, JPG e PDF. PSD, AI, TIFF e CDR entram, mas
// o navegador não abre — o cliente informa o comprimento e o arquivo passa por
// tratamento antes de imprimir.
const ACEITA={
folha: /[.](png|jpe?g|tiff?|pdf|psd|psb|ai|cdr)$/i,
avulsa: /[.](png|jpe?g|webp)$/i
};
const MEDE=/[.](png|jpe?g|webp|gif|bmp|pdf)$/i;
const DPI_RECUSA=96, DPI_AVISA=300; // régua dos designers · 02/09/2026
// A especificação do PDF limita a página a 14.400 unidades — 200 pol, 508 cm.
// Passar disso só é válido com UserUnit (PDF 1.6+), que a maioria dos programas
// não escreve: o arquivo sai fora do padrão e só o gerador lê direito.
const PDF_MAX_PT=14400, PT_CM=2.54/72;
const extDe=n=>(n.split('.').pop()||'').toUpperCase();
let CLIENTE='SEU NOME AQUI', PEDIDO='000000';
let modo=null, artes=[], recemChegada=null, metros=0, nota=0, reencaixado=false;
let folhas=[]; // [{f, med, rep, m}] · o cliente pode subir várias
const folhaTotalM=()=>folhas.reduce((t,x)=>t+(x.m||0)*(x.rep||1),0);
// O cliente declara o que está mandando escolhendo o produto, em vez de o site
// adivinhar pelo formato do arquivo. A declaração de folha montada é conferida na
// medição: uma imagem sem pixel para ocupar a largura do filme não é uma folha.
// Cada modo por metro tem o seu par de artes avulsas, e o seletor anda nos dois sentidos.
const PAR={file:'avulsa', avulsa:'file', uvfile:'uv', uv:'uvfile'};
let montagemCm=0; // altura da folha montada pelo motor, em cm
let montagemLayout=null; // posições aprovadas de cada cópia, em cm
const ZOOMS=[1,1.5,2,3,4]; let zoomI=0; // ampliação da montagem ao vivo
let pedido=[]; // itens já fechados · o pagamento é um só
// A escolha de entrega vale para o pedido inteiro, não por item. No Tiny, retirada
// grava forma_envio X, transportador DropStar e frete por conta do remetente — igual
// ao que a expedição já recebe hoje, sem criar processo novo.
let entrega={tipo:'retira', cep:'', valor:0, cotado:true, end:{}};
let cliente={cnpj:'', zap:'', mail:''};
// CNPJ com os dois dígitos verificadores · máscara sem conta não vale nada
function cnpjOk(v){
const d=(v||'').replace(/\D/g,'');
if(d.length!==14 || /^(\d)\1{13}$/.test(d)) return false;
const calc=(base,pesos)=>{
const soma=base.split('').reduce((t,n,i)=>t+ +n*pesos[i],0);
const r=soma%11; return r<2? 0 : 11-r;
};
const p1=[5,4,3,2,9,8,7,6,5,4,3,2], p2=[6,...p1];
return calc(d.slice(0,12),p1)===+d[12] && calc(d.slice(0,13),p2)===+d[13];
}
const zapOk=v=>{ const d=(v||'').replace(/\D/g,''); return d.length===10||d.length===11; };
const mailOk=v=>/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test((v||'').trim());
const clienteOk=()=>cnpjOk(cliente.cnpj)&&zapOk(cliente.zap)&&mailOk(cliente.mail);
// Frete cotado pelo CEP não entrega nada sozinho: a transportadora e o Tiny
// precisam do endereço completo antes do pagamento.
const UFS='AC AL AP AM BA CE DF ES GO MA MT MS MG PA PB PR PE PI RJ RN RS RO RR SC SP SE TO'.split(' ');
const enderecoOk=()=>{
if(entrega.tipo!=='frete') return true;
const e=entrega.end||{}, t=v=>(v||'').trim();
return t(e.nome).length>=2 && t(e.rua).length>=2 && t(e.num).length>=1 &&
t(e.bairro).length>=2 && t(e.cidade).length>=2 && UFS.includes(t(e.uf).toUpperCase());
};
const destinoApi=()=>entrega.tipo!=='frete' ? null : {
recipient:entrega.end.nome.trim(), street:entrega.end.rua.trim(), number:entrega.end.num.trim(),
complement:(entrega.end.comp||'').trim(), district:entrega.end.bairro.trim(),
city:entrega.end.cidade.trim(), state:entrega.end.uf.trim().toUpperCase(), postal_code:entrega.cep};
// Tempo de envio antes de medir a internet de quem compra: uma faixa entre um
// upload lento (10 Mbps) e um rápido (150 Mbps). O arquivo vai direto para o
// armazenamento, então é o upload do cliente que manda.
const ENVIO_LENTO=10e6/8, ENVIO_RAPIDO=150e6/8, ENVIO_AVISA_S=120;
const duracao=s=>{
const min=Math.ceil(s/60);
return s<60 ? 'menos de 1 min' : min<60 ? min+' min' : Math.floor(min/60)+' h'+(min%60? ' '+min%60+' min' : '');
};
const faixaEnvio=bytes=>bytes/ENVIO_RAPIDO<60 ? 'até '+duracao(bytes/ENVIO_LENTO)
: 'de '+duracao(bytes/ENVIO_RAPIDO)+' a '+duracao(bytes/ENVIO_LENTO);
// Só enquanto há arquivos no produto aberto: sai quando o item vai para o carrinho.
function pintaAvisoEnvio(){
const el=$('avisoEnvio'); if(!el) return;
const total=modo ? (ehFolha()?folhas:artes).reduce((t,x)=>t+(x.f?.size||0),0) : 0;
el.hidden = total/ENVIO_LENTO < ENVIO_AVISA_S;
if(el.hidden) return;
el.innerHTML='<b>Envio: '+faixaEnvio(total)+'</b> ('+fmt(total)+'), conforme sua internet. '+
'Mantenha a página aberta.';
}
const larguraFilme=()=>MODOS[modo].larg;
// cobrança proporcional: arredonda para cima em 10 cm · 2,75 m vira 2,80 m
const cobrar=m=>Math.max(MINIMO_M, Math.ceil(m*10)/10);
const fmtM=m=>m.toFixed(2).replace('.',',');
const precoBase=n=>
(FAIXAS[modo].find(f=>n>=f[0])||FAIXAS[modo][FAIXAS[modo].length-1])[1];
const descontoMetragem=()=>0; // sem progressão por quantidade
const cls=n=>n>=85?'ok':n>=50?'av':'er';
const dpiDe=a=>a.cm>0? (a.giro?a.py:a.px)/(a.cm/2.54) : 0;
// girar 90° inverte a proporção · espelhar não muda medida, só o desenho
const propDe=a=>{ const p=a.prop||1; return a.giro? 1/p : p; };
const TAMANHOS={57:[10,15,20,25,28.2], 28.5:[5,8,10,14,28.2]};