- PNG e JPG entram como artes avulsas. Informe largura e quantidade;
- montamos a folha, com o preço de artes avulsas.
-
+
+
+
+
-
-
A nota fiscal sai no CNPJ informado. O arquivo fica guardado por 90 dias e o histórico
do pedido por 12 meses, para você repetir sem subir de novo.
+
+
+
+
+
@@ -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. '+
+ 'Enviar como '+MODOS[destino].tit.toLowerCase()+'');
+ 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);