diff --git a/dtf-site.html b/dtf-site.html index 0d3c0d5..d8b31e0 100644 --- a/dtf-site.html +++ b/dtf-site.html @@ -577,6 +577,8 @@ h1 em{font-style:normal;color:var(--laranja)} /* ── Carrinho no padrão de checkout: entrega à esquerda, resumo fixo à direita ── */ .carr{display:none;margin-top:18px} +.checkout{margin-top:14px} +.checkout #checkoutStatus:empty{display:none} .carr.on{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;align-items:start} @media(max-width:900px){.carr.on{grid-template-columns:1fr}} .carr h3{font-family:"Inter",sans-serif;font-size:19px;font-weight:700;margin-bottom:12px} @@ -780,10 +782,10 @@ footer a:hover{color:var(--laranja2)}
- Impressão DTF + Impressão DTF
- DTF Têxtil · 57 cm - DTF UV · 28,5 cm + DTF Têxtil · 57 cm + DTF UV · 28,5 cm
@@ -867,7 +869,7 @@ footer a:hover{color:var(--laranja2)}

Mande a arte, veja a nota em segundos e a folha montada antes de fechar. Revisão e remanejamento por nossa conta.

- Enviar minha arte + Enviar minha arte sem cadastro · resposta em segundos
@@ -1000,13 +1002,22 @@ footer a:hover{color:var(--laranja2)}
- + +
+

+
+
@@ -1418,6 +1433,11 @@ 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 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ó @@ -1458,14 +1478,10 @@ const px=f=>Math.round(Math.min(6000,Math.max(600,Math.sqrt(f.size/1024)*95))); // ── escolha document.querySelectorAll('.ec').forEach(b=>b.addEventListener('click',()=>abrir(b.dataset.modo))); -document.querySelectorAll('[data-modo-cta]').forEach(b=>b.addEventListener('click',e=>{ - e.preventDefault(); abrir(b.dataset.modoCta); -})); function abrir(m){ modo=m; artes=[]; folhas=[]; metros=0; nota=0; reencaixado=false; montagemCm=0; - $('imagemComoFolha').checked=false; - $('imagemComoFolha').disabled=false; + avisoTipo(''); pintaModo(); $('lista').innerHTML=''; $('rA').style.display='none'; recusa([]); recemChegada=null; caminho='auto'; avisoCam(''); pintaCaminhos(); @@ -1484,10 +1500,64 @@ function pintaModo(){ $('inp').accept = ehFolha(m) ? '.png,.jpg,.jpeg,.pdf' : '.png,.jpg,.jpeg,.webp'; $('catFoco').className='cat '+(ehFolha(m)?'file':'av'); - $('trocarParaAvulsa').hidden=!ehFolha(m); $('larguraFolhaPronta').textContent=String(c.larg).replace('.',','); $('foco2').classList.remove('sofila'); // a caixa de requisitos vale nos 4 modos $('montcabTit').textContent = ehFolha(m) ? 'Sua folha' : 'Montagem ao vivo'; + pintaTipoEnvio(); +} +// O par folha montada / artes separadas fica visível e com preço nos dois lados, +// nos dois modos: nenhuma troca acontece sem o cliente ver o que muda, e sempre +// dá para voltar atrás sem recomeçar o pedido. +function pintaTipoEnvio(){ + const el=$('tipoEnvio'); if(!el) return; + el.hidden=!modo; + if(el.hidden) return; + const folha = ehFolha() ? modo : PAR[modo]; + const avulsa = ehFolha() ? PAR[modo] : modo; + $('tipoFolhaPreco').textContent='a partir de '+rs(pisoEscada(folha)); + $('tipoAvulsaPreco').textContent='a partir de '+rs(pisoEscada(avulsa))+' · montagem inclusa'; + $('larguraFolhaPronta').textContent=String(MODOS[folha].larg).replace('.',','); + const atual = ehFolha() ? 'folha' : 'avulsa'; + el.querySelectorAll('.cam').forEach(b=>{ + b.classList.toggle('on', b.dataset.tipo===atual); + b.disabled=folhas.length>0 || artes.length>0; // já tem arquivo: a escolha está feita + }); +} +function avisoTipo(txt){ + const el=$('avisoTipo'); if(!el) return; + el.innerHTML=txt; el.style.display = txt? '' : 'none'; +} +// A imagem foi declarada folha montada mas não tem resolução para isso. Devolve o +// arquivo com o motivo e o caminho certo — sem trocar preço por conta própria. +function recusaFolha(x, md){ + const arquivo=x.f, destino = modo==='uvfile' ? 'uv' : 'avulsa'; + folhas=folhas.filter(y=>y!==x); + pintaFolha(); + avisoTipo(''+escapeHTML(arquivo.name)+' não é uma folha montada. '+ + 'Para ocupar os '+n1(larguraFilme())+' cm do filme ela teria só '+md.dpiFolha+ + ' DPI, e o mínimo para imprimir é '+DPI_RECUSA+'. '+ + 'Se for uma arte para montarmos, use '+MODOS[destino].tit+', '+ + 'a partir de '+rs(pisoEscada(destino))+'/m. '+ + ''); + const botao=$('usarAvulsa'); + if(botao) botao.addEventListener('click',()=>{ + avisoTipo(''); trocaTipo(PAR[modo]); sel([arquivo]); + }); +} +document.querySelectorAll('#tipoEnvio .cam').forEach(b=>b.addEventListener('click',()=>{ + if(b.disabled) return; + const querFolha=b.dataset.tipo==='folha'; + if(querFolha===ehFolha()) return; + avisoTipo(''); trocaTipo(PAR[modo]); // escolha explícita do cliente +})); +// Troca declarada, nunca silenciosa: o cliente já viu os dois preços no seletor. +function trocaTipo(destino){ + if(!destino || modo===destino) return; + folhas=[]; artes=[]; recemChegada=null; metros=0; nota=0; montagemCm=0; + modo=destino; + $('lista').innerHTML=''; $('rA').style.display='none'; recusa([]); + caminho='auto'; avisoCam(''); + pintaModo(); pintaCaminhos(); limpaPaineis(); agendaAvaliacao(); previaAoVivo(); } $('bVoltar').addEventListener('click',()=>{ itemAtual=null; $('atual').style.display='none'; $('carrLin').innerHTML=''; pintaPedido(); @@ -1525,18 +1595,9 @@ Z.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){I.click();e.pr I.addEventListener('change',()=>sel([...I.files])); function sel(fs){ if(!fs.length) return; - // Pixels cannot tell a loose artwork from a finished sheet. Images default - // to the artwork model unless the customer explicitly declares a ready sheet. - // Update this same editor in place; do not reopen it or discard uploaded files. - if(ehFolha() && !$('imagemComoFolha').checked && fs.some(f=>ACEITA.avulsa.test(f.name))){ - if(fs.some(f=>!ACEITA.avulsa.test(f.name))){ - $('recusa').style.display='block'; - $('recusa').textContent='Envie artes avulsas e folhas prontas separadamente. Para uma imagem já montada, marque a opção de folha pronta.'; - return; - } - modo=modo==='uvfile'?'uv':'avulsa'; - pintaModo(); pintaCaminhos(); - } + // Pixels cannot tell a loose artwork from a finished sheet, so the customer + // declares it by choosing the product in the selector above. The declaration is + // verified in medirFolha; the file never changes product or price on its own. const regra = ehFolha() ? ACEITA.folha : ACEITA.avulsa; const fora = fs.filter(f=>!regra.test(f.name)); fs = fs.filter(f=>regra.test(f.name)); @@ -1559,12 +1620,12 @@ function sel(fs){ pintaCaminhos(); } const novas=fs.map(f=>({f, med:null, rep:1, m:0, semAnalise:null})); - $('imagemComoFolha').checked=true; folhas=folhas.concat(novas); // soma, não substitui pintaFolha(); novas.forEach(x=>{ x.pct=5; pintaFolha(); medirFolha(x.f).then(md=>{ + if(md && md.dpiFolha!=null && md.dpiFolha{ @@ -1846,7 +1907,11 @@ function medirFolha(file){ carregarImagem(file).then(img=>{ if(!img || !img.width) return res(null); const L=larguraFilme(); - res({larg:L, alt:+(img.height/img.width*L).toFixed(1), fonte:'proporção da imagem'}); + // Uma imagem só é folha montada se tiver pixel para ocupar a largura do + // filme. Sem esta conferência, qualquer arte pequena vira "folha" e leva + // o preço por metro sem nunca ter sido montada. + res({larg:L, alt:+(img.height/img.width*L).toFixed(1), fonte:'proporção da imagem', + dpiFolha:Math.round(img.width/(L/2.54)), px:img.width}); }); return; } @@ -1882,8 +1947,7 @@ function medirFolha(file){ function pintaFolha(){ const L=larguraFilme(); - $('imagemComoFolha').disabled=folhas.length>0; - if(!folhas.length) $('imagemComoFolha').checked=false; + pintaTipoEnvio(); // trava o seletor enquanto houver folha if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; } $('lista').innerHTML = folhas.map((x,i)=>{ @@ -2769,7 +2833,7 @@ function pintaCaminhos(){ const ehF=modo&&ehFolha(); $('caminhos').style.display = ehF? '' : 'none'; if(!ehF) return; - document.querySelectorAll('.cam').forEach(b=>b.classList.toggle('on', b.dataset.cam===caminho)); + document.querySelectorAll('#caminhos .cam').forEach(b=>b.classList.toggle('on', b.dataset.cam===caminho)); $('camA').textContent='a partir de '+rs(pisoEscada()); $('camB').textContent=rs(TABELA[modo])+' fixo'; $('inp').accept = caminho==='auto' ? '.png,.jpg,.jpeg,.pdf' @@ -2778,7 +2842,7 @@ function pintaCaminhos(){ ? 'PNG, JPG ou PDF · conferência automática, nota na tela' : 'CDR, AI, PSD ou TIFF · sem conferência, metro pela tabela'; } -document.querySelectorAll('.cam').forEach(b=>b.addEventListener('click',()=>{ +document.querySelectorAll('#caminhos .cam').forEach(b=>b.addEventListener('click',()=>{ caminho=b.dataset.cam; avisoCam(''); pintaCaminhos(); })); function avisoCam(txt){ diff --git a/local/artwork_browser_test.mjs b/local/artwork_browser_test.mjs index f23af6d..b40a1f2 100644 --- a/local/artwork_browser_test.mjs +++ b/local/artwork_browser_test.mjs @@ -103,9 +103,28 @@ try{ if(drop)$('zona').dispatchEvent(new DragEvent('drop',{dataTransfer:dt,bubbles:true,cancelable:true})); else{$('inp').files=dt.files;$('inp').dispatchEvent(new Event('change',{bubbles:true}));} }; + // A genuine mounted sheet spans the film width with printable resolution. + window.sendSheet=async()=>{ + const cm=larguraFilme(), px=Math.ceil(cm/2.54*150); + const c=document.createElement('canvas');c.width=px;c.height=Math.round(px*0.6); + const ctx=c.getContext('2d');ctx.fillStyle='white';ctx.fillRect(0,0,c.width,c.height); + ctx.fillStyle='#222';ctx.fillRect(40,40,px/4,px/4); + const blob=await new Promise(r=>c.toBlob(r,'image/png')); + const dt=new DataTransfer();dt.items.add(new File([blob],'folha-montada.png',{type:'image/png'})); + $('inp').files=dt.files;$('inp').dispatchEvent(new Event('change',{bubbles:true})); + }; + window.pickTipo=t=>document.querySelector('#tipoEnvio .cam[data-tipo="'+t+'"]').click(); })()`); // Reproduce the reported entry path, with no navigation workaround. + // Navigation links must reach the chooser, never preselect a product. + assert.equal(await evaluate(`document.querySelectorAll('[data-modo-cta]').length`),0); await click('[data-modo="file"]'); + // By-metre opens declaring a mounted sheet; switching is explicit and priced. + assert.deepEqual(await evaluate(`({shown:!$('tipoEnvio').hidden, + on:[...document.querySelectorAll('#tipoEnvio .cam.on')].map(b=>b.dataset.tipo)})`), + {shown:true,on:['folha']}); + await evaluate(`pickTipo('avulsa')`); + assert.equal(await evaluate('modo'),'avulsa'); await evaluate('sendImage()'); await waitFor(()=>evaluate(`artes.length===1 && !!artes[0].src`),'JPG artwork model'); assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,width:artes[0].cm,quantity:artes[0].q})`),{mode:'avulsa',sheets:0,width:0,quantity:1}); @@ -134,26 +153,35 @@ try{ const m=result.pos[0].m; return !m.bits[2*m.cw+2] && !m.bits[2*m.cw+30] && !!m.bits[15*m.cw+30]; })()`),true); - // Explicit finished-sheet images retain their separate dimensions and price. + // A real finished sheet keeps its own dimensions and by-metre price. await click('#bVoltar');await click('[data-modo="file"]'); - await click('#imagemComoFolha');await evaluate('sendImage()'); - await waitFor(()=>evaluate('folhas.length===1 && !!folhas[0].previewSrc && metros===1.14'),'ready-sheet image'); + await evaluate('sendSheet()'); + await waitFor(()=>evaluate('folhas.length===1 && !!folhas[0].previewSrc && metros===0.342'),'ready-sheet image'); assert.deepEqual(await evaluate(`({mode:modo,art:artes.length,title:$('montcabTit').textContent,rate:precoBase(100)})`),{mode:'file',art:0,title:'Sua folha',rate:14.9}); assert.equal(await evaluate(`document.querySelectorAll('[data-cm]').length`),0); + // With a sheet loaded the declaration is locked; it cannot flip under the customer. + assert.equal(await evaluate(`document.querySelector('#tipoEnvio .cam[data-tipo="avulsa"]').disabled`),true); await evaluate(`(()=>{const el=$('lista').querySelector('[data-repf]');el.value=2;el.dispatchEvent(new Event('change',{bubbles:true}));})()`); - await waitFor(()=>evaluate('metros===2.28'),'ready-sheet repetitions'); + await waitFor(()=>evaluate('metros===0.684'),'ready-sheet repetitions'); assert.equal(await evaluate(`document.querySelectorAll('.folhaPrevia').length`),1); - // Keep manual ready-sheet uploads intact and refuse ambiguous mixed batches. + // An image too small to span the film is refused, never silently repriced. + await click('#bVoltar');await click('[data-modo="file"]'); + await evaluate('sendImage()'); + await waitFor(()=>evaluate(`!!document.getElementById('usarAvulsa')`),'ready-sheet refusal'); + assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,art:artes.length})`),{mode:'file',sheets:0,art:0}); + await click('#usarAvulsa'); + await waitFor(()=>evaluate(`modo==='avulsa' && artes.length===1`),'one-click recovery into loose artwork'); + // Keep manual ready-sheet uploads intact. await click('#bVoltar');await click('[data-modo="file"]'); - await evaluate(`sel([new File(['sheet'],'sheet.cdr'),new File(['image'],'art.png')])`); - assert.deepEqual(await evaluate('[folhas.length,artes.length]'),[0,0]); await evaluate(`sel([new File(['sheet'],'sheet.cdr')])`); await waitFor(()=>evaluate(`!!$('lista').querySelector('[data-comp]')`),'manual ready sheet'); await evaluate(`(()=>{const el=$('lista').querySelector('[data-comp]');el.value=1.01;el.dispatchEvent(new Event('change',{bubbles:true}));})()`); await waitFor(()=>evaluate('itemAtual?.total===21.89'),'unchanged manual pricing'); - // PNG drag/drop follows exactly the same artwork path; UV stays UV. - for(const [mode,expected] of [['file','avulsa'],['avulsa','avulsa'],['uvfile','uv']]){ + // PNG drag/drop follows exactly the same artwork path; UV stays UV. Reaching it + // from a by-metre product is one declared click, and it is reversible. + for(const [mode,expected] of [['file','avulsa'],['avulsa','avulsa'],['uvfile','uv'],['uv','uv']]){ await click('#bVoltar');await click('[data-modo="'+mode+'"]'); + if(await evaluate(`ehFolha()`))await evaluate(`pickTipo('avulsa')`); await evaluate(`sendImage('image/png',true)`); await waitFor(()=>evaluate('artes.length===1 && !!artes[0].src'),'PNG drop'); assert.equal(await evaluate('modo'),expected);