Files
dtf-system/web/site-config.js
Cauê Faleiros 8dddf0fa25
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m31s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m0s
feat: redesign the Montagem page, with thumbnails, check chips and a DPI price scale
The product page is rebuilt around mockup 1. The two ways to send are
cards under the title; the PNG/CDR route choice is gone (it never changed
the price, and the picker now takes every allowed format). Each file card
has a thumbnail, its size, check chips with a plain sentence for any
problem, a "Ver detalhes da conferência" link and a copies stepper; a CDR
file says "Conferência manual · preço cheio" and asks for the length. The
buy box shows the price scale in DPI with the customer's step, the metre
and 10 cm length, the total and what the resolution saved, and "Sai em N
partes" only above 20 m. Folha já montada gets "Ampliar" instead of zoom.
Pricing, grade and checks are unchanged.

Fixes on top: the card thumbnail is a small image made once, not the full
preview re-parsed on every repaint; sizes from 1 GB up read in GB; a failed
upload is shown with "Tentar de novo" instead of retrying silently on every
cart change. The browser suites wait for the product and the reloaded page
instead of racing them, and the artwork suite delays imagemDaArte, which
the packing now uses, fixing the CI failure of f4aacb5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:36:41 -03:00

111 lines
6.5 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('.',',');
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};
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]};