From 67aa6c970cf3e1642e154a7c32390c806d9eae42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 18 Sep 2026 18:32:38 -0300 Subject: [PATCH] fix: make the by-metre product reachable and its declaration verified Navigation links carried data-modo-cta and opened loose artwork directly, so every entry point except the price card bypassed "Arquivo por metro". Dropping a PNG or JPG from the by-metre editor then switched the order to loose artwork and repriced it, while the same screen advertised PNG/JPG in its drop zone and marked that path as the cheaper one. Replace the guessing with a declaration: the product is the choice. #tipoEnvio shows ready sheet and loose artwork side by side with both prices, in all four modes, reversible until a file is attached and locked afterwards. sel() no longer reassigns modo, so a file can never change product or price on its own. Verify the declaration instead of trusting it. medirFolha returns dpiFolha, and an image without the pixels to span the film width at DPI_RECUSA is refused as a sheet, with the numbers shown and one click to send it as loose artwork. Previously a small image declared as a sheet was billed by the metre. Scope pintaCaminhos to #caminhos .cam: its global selector was clearing the new control. Move the checkout status out of #carr, which the success path hides, so a paid order still confirms itself to the customer. Co-Authored-By: Claude Opus 5 --- dtf-site.html | 136 ++++++++++++++++++++++++--------- local/artwork_browser_test.mjs | 46 ++++++++--- 2 files changed, 137 insertions(+), 45 deletions(-) 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)}
@@ -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);