fix: harden week-two ordering, artwork and operations
This commit is contained in:
@@ -6,6 +6,23 @@
|
||||
let draftId = localStorage.getItem('dtf-quote');
|
||||
let requestKey = localStorage.getItem('dtf-request-key');
|
||||
let requestBody = localStorage.getItem('dtf-request-body');
|
||||
let quotedCart = localStorage.getItem('dtf-quote-cart');
|
||||
let refreshVersion = 0;
|
||||
function cartSnapshot() {
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
return JSON.stringify({customer:cliente,delivery:entrega,items:items.map(item=>({
|
||||
mode:item.modo,metres:item.metros,grade:item.nota,production:item.production,
|
||||
quality:item.qualityStatus,
|
||||
acknowledged:item.qualityAcknowledged,
|
||||
files:(item.localFiles||[]).map(file=>({name:file.name,size:file.size,lastModified:file.lastModified}))
|
||||
}))});
|
||||
}
|
||||
function clearDraft() {
|
||||
draftId=null;quotedCart=null;requestKey=null;requestBody=null;
|
||||
for(const key of ['dtf-quote','dtf-quote-cart','dtf-request-key','dtf-request-body'])
|
||||
localStorage.removeItem(key);
|
||||
actions.replaceChildren();
|
||||
}
|
||||
const api = async (path, body) => {
|
||||
const response = await fetch('/api'+path, {
|
||||
credentials: 'same-origin', headers: {'Content-Type':'application/json'},
|
||||
@@ -19,6 +36,12 @@
|
||||
return data;
|
||||
};
|
||||
const ready = api('/session');
|
||||
ready.then(session=>{
|
||||
window.dtfUploadMaxBytes=session.max_upload_bytes;
|
||||
const limit=document.getElementById('zLimite');
|
||||
if(limit)limit.textContent='Até '+(session.max_upload_bytes/1048576).toFixed(0)+
|
||||
' MB por arquivo enquanto a verificação de segurança para arquivos grandes é preparada.';
|
||||
}).catch(()=>{});
|
||||
window.dtfSessionReady=ready;
|
||||
window.dtfApi=api;
|
||||
ready.catch(error => { status.textContent = error.message; });
|
||||
@@ -50,8 +73,10 @@
|
||||
if (busy) return;
|
||||
if (draftId) { await refresh(); status.scrollIntoView({behavior:'smooth',block:'nearest'}); return; }
|
||||
if (!clienteOk() || !entrega.cotado) return;
|
||||
if (!cartPodeEnviar()) return message('Revise a qualidade e confirme a ressalva de cada item antes de enviar o pedido.');
|
||||
const cart = [...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
if (!cart.length) return message('Adicione um item ao pedido.');
|
||||
const initialCart=cartSnapshot();
|
||||
busy = true;
|
||||
$('bPagar').disabled = true;
|
||||
try {
|
||||
@@ -62,9 +87,15 @@
|
||||
if (!item.localFiles?.length) throw new Error('Selecione novamente os arquivos deste item.');
|
||||
const uploads=[];
|
||||
for (const file of item.localFiles) uploads.push(await upload(file));
|
||||
items.push({mode:item.modo, metres:String(item.metros), grade:item.nota, uploads});
|
||||
if (item.production?.version!==2 || item.production.sources?.length!==uploads.length)
|
||||
throw new Error('A montagem deste item precisa ser refeita antes da cotação.');
|
||||
items.push({mode:item.modo, metres:String(item.metros), grade:item.nota, uploads,
|
||||
production:{...item.production,sources:item.production.sources.map((source,index)=>({
|
||||
upload_id:uploads[index],...source}))},
|
||||
quality_status:item.qualityStatus,quality_acknowledged:item.qualityAcknowledged});
|
||||
}
|
||||
const content = {customer:{...cliente},items,freight:{service:entrega.tipo==='retira'?'pickup':'mock-standard',postal_code:entrega.tipo==='retira'?'':entrega.cep}};
|
||||
if (cartSnapshot()!==initialCart) throw new Error('O carrinho mudou durante o envio. Confira os itens e envie de novo.');
|
||||
const serialized = JSON.stringify(content);
|
||||
if (!requestKey || serialized !== requestBody) {
|
||||
requestKey = crypto.randomUUID(); requestBody = serialized;
|
||||
@@ -72,6 +103,7 @@
|
||||
localStorage.setItem('dtf-request-body',requestBody);
|
||||
}
|
||||
const quote = await api('/quotes',{request_key:requestKey,...content});
|
||||
quotedCart=initialCart;localStorage.setItem('dtf-quote-cart',quotedCart);
|
||||
draftId=quote.id; localStorage.setItem('dtf-quote',draftId);
|
||||
await refresh();
|
||||
status.scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
@@ -80,10 +112,17 @@
|
||||
};
|
||||
async function refresh() {
|
||||
if (!draftId) return;
|
||||
const version=++refreshVersion, shownId=draftId;
|
||||
try {
|
||||
await ready;
|
||||
const quote=await api('/quotes/'+draftId);
|
||||
const quote=await api('/quotes/'+shownId);
|
||||
if(version!==refreshVersion || draftId!==shownId) return;
|
||||
actions.replaceChildren();
|
||||
if (quote.status!=='paid' && (!quotedCart || quotedCart!==cartSnapshot())) {
|
||||
message('O carrinho mudou ou não está disponível neste navegador. A cotação anterior continua separada; envie o carrinho atual para uma nova revisão.');
|
||||
button('Enviar carrinho atual',()=>{clearDraft();window.dtfCheckout();});
|
||||
return;
|
||||
}
|
||||
if (quote.status==='pending_review') {
|
||||
message('Arquivos enviados. No Kanban, confira metragem e nota e aprove a cotação '+draftId.slice(0,8)+'.');
|
||||
} else if (quote.status==='approved') {
|
||||
@@ -93,6 +132,7 @@
|
||||
return;
|
||||
}
|
||||
button('Criar pedido de teste',async event=>{
|
||||
if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; }
|
||||
event.target.disabled=true;
|
||||
try {
|
||||
const order=await api('/orders/dev-paid',{quote_id:draftId});
|
||||
@@ -104,18 +144,23 @@
|
||||
});
|
||||
} else if (quote.status==='paid') {
|
||||
message('Pedido #'+quote.order.number+' · etapa: '+quote.order.state+'.');
|
||||
button('Novo pedido',()=>{localStorage.removeItem('dtf-quote');localStorage.removeItem('dtf-request-key');localStorage.removeItem('dtf-request-body');location.reload();});
|
||||
button('Novo pedido',()=>{clearDraft();location.reload();});
|
||||
} else {
|
||||
message('Cotação expirada. Envie o carrinho para uma nova revisão.');
|
||||
button('Nova cotação',()=>{draftId=null;requestKey=null;localStorage.removeItem('dtf-quote');localStorage.removeItem('dtf-request-key');actions.replaceChildren();});
|
||||
button('Nova cotação',clearDraft);
|
||||
}
|
||||
} catch(error) {
|
||||
if(version!==refreshVersion || draftId!==shownId) return;
|
||||
message(error.message);
|
||||
actions.replaceChildren();
|
||||
button('Limpar referência e tentar de novo',()=>{draftId=null;requestKey=null;localStorage.removeItem('dtf-quote');localStorage.removeItem('dtf-request-key');actions.replaceChildren();});
|
||||
button('Limpar referência e tentar de novo',clearDraft);
|
||||
}
|
||||
}
|
||||
const quoteFromPortal=new URLSearchParams(location.search).get('quote');
|
||||
if(quoteFromPortal && /^[0-9a-f-]{36}$/.test(quoteFromPortal)){draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId);}
|
||||
if(quoteFromPortal && /^[0-9a-f-]{36}$/.test(quoteFromPortal)){
|
||||
if(draftId!==quoteFromPortal){quotedCart=null;localStorage.removeItem('dtf-quote-cart');}
|
||||
draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId);
|
||||
}
|
||||
window.addEventListener('dtf-cart-changed',()=>{if(draftId) refresh();});
|
||||
refresh();
|
||||
})();
|
||||
|
||||
@@ -1035,6 +1035,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div class="zona" id="zona" tabindex="0" role="button">
|
||||
<div class="ico">↑</div>
|
||||
<b id="zTit">Arraste aqui</b><span id="zSub"></span>
|
||||
<span id="zLimite">Até 128 MB por arquivo enquanto a verificação de segurança para arquivos grandes é preparada.</span>
|
||||
<span class="bt">ou escolher no computador</span>
|
||||
</div>
|
||||
<div class="recusa" id="recusa" style="display:none"></div>
|
||||
@@ -1098,9 +1099,8 @@ footer a:hover{color:var(--laranja2)}
|
||||
<li class="s"><b>PDF é o que conferimos melhor.</b> Nele medimos a resolução de
|
||||
cada imagem dentro da folha, uma por uma. Se a sua arte é boa, é onde a nota
|
||||
aparece com mais precisão</li>
|
||||
<li class="a"><b>PDF só até 5 m.</b> O limite é do próprio formato: a página do PDF
|
||||
para em 508 cm. Acima disso o arquivo sai fora do padrão e só abre certo em quem
|
||||
o gerou — divida em partes ou mande PNG</li>
|
||||
<li class="a"><b>PDF grande.</b> Sem UserUnit, a página passa do limite de 508 cm
|
||||
do formato; divida a folha ou mande PNG. Envie um PDF de uma página por folha.</li>
|
||||
<li class="a"><b>TIFF, PSD, AI e CDR</b> a gente aceita, mas ninguém consegue
|
||||
conferir sozinho: alguém abre na mão, sai mais devagar e o metro vai pela
|
||||
tabela, sem desconto de nota</li>
|
||||
|
||||
@@ -3,6 +3,8 @@ const $ = id=>document.getElementById(id);
|
||||
// Remove credentials saved by older local builds. Only HttpOnly sessions now.
|
||||
sessionStorage.removeItem('dtf-operator');
|
||||
let board;
|
||||
let extraQuotes={pending:[],approved:[]};
|
||||
let moreQuotes={pending:false,approved:false};
|
||||
const money=cents=>(cents/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
|
||||
function node(tag,text,className){const e=document.createElement(tag);if(text!==undefined)e.textContent=text;if(className)e.className=className;return e;}
|
||||
function action(text,fn){const b=node('button',text);b.onclick=async()=>{b.disabled=true;try{await fn();}catch(e){$('status').textContent=e.message;}finally{b.disabled=false;}};return b;}
|
||||
@@ -16,25 +18,67 @@ function files(container,items){
|
||||
const link=node('a');link.href=result.url;link.referrerPolicy='no-referrer';link.download=result.name;link.click();
|
||||
}));
|
||||
}
|
||||
function productionLines(container,item){
|
||||
if(!item.production){container.append(node('p','Instruções de produção ausentes; não produzir este item.','meta'));return;}
|
||||
container.append(node('p','Especificação v'+item.production.version+' · qualidade '+item.quality_status+
|
||||
(item.quality_status==='warning'?' · ressalva '+(item.quality_acknowledged?'aceita':'não aceita'):''),'meta'));
|
||||
if(item.production.placements){
|
||||
container.append(node('p',item.production.placements.length+' peças posicionadas em '+
|
||||
item.production.film_width_cm+' × '+item.production.height_cm+' cm de filme','meta'));
|
||||
const download=node('button','Baixar manifesto da montagem');
|
||||
download.type='button';
|
||||
download.onclick=()=>{
|
||||
const url=URL.createObjectURL(new Blob([JSON.stringify(item.production,null,2)],{type:'application/json'}));
|
||||
const link=node('a');link.href=url;
|
||||
link.download='dtf-layout-'+item.production.sources[0].upload_id.slice(0,8)+'.json';
|
||||
link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||
};
|
||||
container.append(download);
|
||||
}
|
||||
item.production.sources.forEach((source,index)=>container.append(node('p',
|
||||
(index+1)+'. '+source.kind+' · '+source.copies+' × '+source.width_cm+' × '+source.length_cm+
|
||||
' cm · giro '+source.rotation_degrees+'°'+(source.mirrored?' · espelhada':'')+
|
||||
' · medida '+source.measurement+' · arquivo '+source.upload_id.slice(0,8),'meta')));
|
||||
}
|
||||
async function load(){
|
||||
try{
|
||||
board=await api('/board');$('login').hidden=true;
|
||||
board=await api('/board');
|
||||
extraQuotes={pending:[],approved:[]};
|
||||
moreQuotes={pending:board.pending_total>board.quotes.filter(q=>!q.approved).length,
|
||||
approved:board.approved_total>board.quotes.filter(q=>!!q.approved).length};
|
||||
$('login').hidden=true;
|
||||
$('status').textContent='Atualizado às '+new Date().toLocaleTimeString();
|
||||
render();
|
||||
}catch(e){$('status').textContent=e.message;$('login').hidden=false;}
|
||||
}
|
||||
async function loadMoreQuotes(kind){
|
||||
const shown=[...board.quotes.filter(q=>kind==='pending'?!q.approved:!!q.approved),...extraQuotes[kind]];
|
||||
const last=shown.at(-1);
|
||||
const params=new URLSearchParams({kind,limit:'50'});
|
||||
if(last){params.set('before_created_at',last.created_at);params.set('before_id',last.id);}
|
||||
const page=await api('/quotes?'+params);
|
||||
const known=new Set(shown.map(q=>q.id));
|
||||
extraQuotes[kind].push(...page.quotes.filter(q=>!known.has(q.id)));
|
||||
moreQuotes[kind]=page.has_more;
|
||||
render();
|
||||
}
|
||||
function render(){
|
||||
$('reviews').replaceChildren();
|
||||
if(board.quotes.length)$('reviews').append(node('h2','Cotações · conferência comercial'));
|
||||
for(const quote of board.quotes){
|
||||
if(board.pending_total || board.approved_total)
|
||||
$('reviews').append(node('h2','Cotações · '+board.pending_total+' pendentes · '+
|
||||
board.approved_total+' aprovadas sem pedido'));
|
||||
for(const quote of [...board.quotes,...extraQuotes.pending,...extraQuotes.approved]){
|
||||
const card=node('article',undefined,'review');
|
||||
card.append(node('b',quote.id.slice(0,8)+' · '+quote.draft.customer.mail));
|
||||
if(quote.status!=='pending_review'){
|
||||
if(quote.status==='pending_review' && quote.draft.items.some(item=>item.production?.version!==2)){
|
||||
card.append(node('p','Cotação com montagem antiga. Peça ao cliente para enviar uma nova cotação.'));
|
||||
}else if(quote.status!=='pending_review'){
|
||||
card.append(node('p',quote.status==='expired'?'Cotação expirada. O cliente precisa solicitar outra.':'Aprovada: '+money(quote.approved.total_cents)+'. Volte ao Site, atualize o pedido e confirme o pagamento local.'));
|
||||
}else{
|
||||
const form=node('form');
|
||||
const edits=quote.draft.items.map((item,index)=>{
|
||||
const row=node('div');row.append(node('strong',(index+1)+'. '+item.mode+' '));
|
||||
productionLines(row,item);
|
||||
const metres=node('input');metres.type='number';metres.min='0.001';metres.max='12000';metres.step='any';metres.value=item.metres;metres.required=true;
|
||||
const grade=node('input');grade.type='number';grade.min='0';grade.max='100';grade.step='1';grade.value=item.grade;grade.required=true;
|
||||
const ml=node('label','Metros conferidos');ml.append(metres);const gl=node('label','Nota conferida');gl.append(grade);row.append(ml,gl);form.append(row);
|
||||
@@ -47,6 +91,10 @@ function render(){
|
||||
}
|
||||
const downloads=node('div',undefined,'actions');files(downloads,quote.draft.items);card.append(downloads);$('reviews').append(card);
|
||||
}
|
||||
for(const [kind,label] of [['pending','Carregar cotações pendentes mais antigas'],
|
||||
['approved','Carregar cotações aprovadas mais antigas']]){
|
||||
if(moreQuotes[kind])$('reviews').append(action(label,()=>loadMoreQuotes(kind)));
|
||||
}
|
||||
$('kan').replaceChildren();
|
||||
const colors=['#00b8da','#e0357c','#efb700','#edebe6','#e0642a','#48b072'];
|
||||
Object.entries(board.states).forEach(([state,title],index)=>{
|
||||
@@ -60,7 +108,10 @@ function render(){
|
||||
for(const order of orders){
|
||||
const card=node('article',undefined,'cd');card.draggable=true;card.dataset.order=order.id;
|
||||
card.append(node('b','#'+order.number+' · Pago local'),node('p',order.snapshot.customer.mail,'meta'),node('p',money(order.snapshot.total_cents)));
|
||||
for(const item of order.snapshot.items)card.append(node('p',item.mode+' · '+item.billed_metres+' m · nota '+item.grade,'meta'));
|
||||
for(const item of order.snapshot.items){
|
||||
card.append(node('p',item.mode+' · '+item.billed_metres+' m · nota '+item.grade,'meta'));
|
||||
productionLines(card,item);
|
||||
}
|
||||
const actions=node('div',undefined,'actions');files(actions,order.snapshot.items);
|
||||
const artwork=node('div');
|
||||
actions.append(action('Arquivos de produção',()=>artworkPanel(order,artwork)));
|
||||
|
||||
@@ -4,7 +4,23 @@
|
||||
scope and run top to bottom, exactly as the original did. */
|
||||
// ── carrinho
|
||||
let itemAtual=null;
|
||||
function invalidaItemAtual(){
|
||||
if(!itemAtual) return;
|
||||
itemAtual=null;
|
||||
$('atual').style.display='none';
|
||||
$('carrLin').innerHTML='';
|
||||
pintaPedido();
|
||||
}
|
||||
function itemPodeEnviar(item){
|
||||
return item.qualityStatus==='ok' || item.qualityStatus==='unverified' ||
|
||||
(item.qualityStatus==='warning' && item.qualityAcknowledged===true);
|
||||
}
|
||||
function cartPodeEnviar(){
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
return items.length>0 && items.every(itemPodeEnviar);
|
||||
}
|
||||
function limpaPaineis(){
|
||||
invalidaItemAtual();
|
||||
['qual','prev'].forEach(id=>$(id).classList.remove('on'));
|
||||
if(!pedido.length){ $('carr').classList.remove('on'); }
|
||||
}
|
||||
@@ -37,6 +53,7 @@ function pintaPedido(){
|
||||
$('bPagar').textContent = (pedido.length + (itemAtual?1:0))>1
|
||||
? 'Pagar os '+(pedido.length+(itemAtual?1:0))+' itens de uma vez'
|
||||
: 'Ir para o pagamento';
|
||||
$('bPagar').disabled = !cartPodeEnviar() || !entrega.cotado || !clienteOk();
|
||||
if(pedido.length || itemAtual) $('carr').classList.add('on');
|
||||
if($('cartLink')) $('cartLink').textContent='Carrinho ('+n+')';
|
||||
window.dispatchEvent(new Event('dtf-cart-changed'));
|
||||
@@ -62,8 +79,36 @@ function carrinho(){
|
||||
'<div class="l" style="color:var(--laranja2)"><span>Revisão e reencaixe</span><b>grátis</b></div>';
|
||||
$('carrLin').innerHTML=l;
|
||||
$('atual').style.display='';
|
||||
const sources=(ehFolha()?folhas:artes).map(x=>ehFolha()
|
||||
? {kind:'sheet',width_cm:x.med?.larg||larguraFilme(),length_cm:+(x.m*100).toFixed(4),
|
||||
copies:x.rep||1,rotation_degrees:0,mirrored:false,
|
||||
measurement:x.med?'file':'customer'}
|
||||
: {kind:'artwork',width_cm:x.cm,length_cm:+(x.cm*propDe(x)).toFixed(4),
|
||||
copies:x.q,rotation_degrees:x.giro||0,mirrored:!!x.esp,
|
||||
measurement:x.src?'file':'customer'});
|
||||
let layout;
|
||||
if(ehFolha()){
|
||||
let y=0;
|
||||
const placements=[];
|
||||
sources.forEach((source,source_index)=>{
|
||||
for(let copy_index=0;copy_index<source.copies;copy_index++){
|
||||
placements.push({source_index,copy_index,x_cm:0,y_cm:+y.toFixed(4),
|
||||
width_cm:source.width_cm,length_cm:source.length_cm,
|
||||
rotation_degrees:0,mirrored:false});
|
||||
y+=source.length_cm;
|
||||
}
|
||||
});
|
||||
layout={height_cm:+y.toFixed(4),placements};
|
||||
}else{
|
||||
if(!montagemLayout) return;
|
||||
layout=montagemLayout;
|
||||
}
|
||||
itemAtual={modo, tit:MODOS[modo].tit, nota, metros, cob, unit, total:tot,
|
||||
localFiles:(ehFolha()?folhas:artes).map(x=>x.f),
|
||||
production:{version:2,film_width_cm:larguraFilme(),sources,...layout},
|
||||
qualityStatus:$('ciente').classList.contains('recusa')?'rejected':
|
||||
$('ciente').classList.contains('on')?'warning':nota===0?'unverified':'ok',
|
||||
qualityAcknowledged:false,
|
||||
desc:fmtM(cob)+' m · '+rs(unit)+'/m · nota '+nota};
|
||||
pintaPedido();
|
||||
previa();
|
||||
@@ -74,4 +119,3 @@ function carrinho(){
|
||||
}
|
||||
}
|
||||
function qm(t,c){ const m=$('qmsg'); m.style.display='block'; m.style.color=c||'var(--verde)'; m.innerHTML=t; }
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const folhaTotalM=()=>folhas.reduce((t,x)=>t+(x.m||0)*(x.rep||1),0);
|
||||
// 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
|
||||
@@ -100,9 +101,7 @@ 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.px/(a.cm/2.54) : 0;
|
||||
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]};
|
||||
const px=f=>Math.round(Math.min(6000,Math.max(600,Math.sqrt(f.size/1024)*95)));
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ function pintaEntrega(){
|
||||
? 'DTF é impresso depois que você paga. Avisamos no WhatsApp quando estiver '+
|
||||
'<b>pronto para retirar</b> — não venha antes do aviso.'
|
||||
: '';
|
||||
$('bPagar').disabled = !entrega.cotado || !clienteOk();
|
||||
$('bPagar').disabled = !entrega.cotado || !clienteOk() || !cartPodeEnviar();
|
||||
pintaPedido();
|
||||
}
|
||||
|
||||
@@ -156,6 +156,10 @@ $('bCep').addEventListener('click',async()=>{
|
||||
});
|
||||
|
||||
$('bMais').addEventListener('click',()=>{
|
||||
if(itemAtual && !itemPodeEnviar(itemAtual)){
|
||||
$('qual').scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
return;
|
||||
}
|
||||
if(itemAtual){ pedido.push(itemAtual); itemAtual=null; }
|
||||
$('atual').style.display='none'; $('carrLin').innerHTML='';
|
||||
$('foco').classList.remove('on'); $('cards').style.display='';
|
||||
@@ -170,4 +174,3 @@ $('bPagar').addEventListener('click',()=>{
|
||||
else alert('O pedido online está indisponível no momento. Tente novamente em instantes.');
|
||||
});
|
||||
pintaEntrega();
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@ function encaixar(pecas,W){
|
||||
}
|
||||
|
||||
const postas=[];
|
||||
if(pecas.some(p=>!Number.isFinite(p.w) || p.w<=0 || p.w>W))
|
||||
throw new RangeError('A largura solicitada não cabe no filme.');
|
||||
const ordem=[...pecas].sort((a,b)=>(b.w*b.h)-(a.w*a.h));
|
||||
|
||||
ordem.forEach(p=>{
|
||||
@@ -173,10 +175,8 @@ function encaixar(pecas,W){
|
||||
}
|
||||
});
|
||||
if(!melhor){
|
||||
const k=Math.min(1, W/p.w); // cabe na largura, nem que seja reduzindo
|
||||
const w=p.w*k, h=p.h*k;
|
||||
const m=mascara(p.img,w,h,p.a?.giro||0,p.a?.esp);
|
||||
melhor={cx:0,cy:linhas,m,x:0,y:linhas*CEL,w,h,rot:0,ref:p};
|
||||
const m=mascara(p.img,p.w,p.h,p.a?.giro||0,p.a?.esp);
|
||||
melhor={cx:0,cy:linhas,m,x:0,y:linhas*CEL,w:p.w,h:p.h,rot:0,ref:p};
|
||||
}
|
||||
// a folga só existe entre peças, não é folha cobrada
|
||||
ocupar(melhor.m,melhor.cx,melhor.cy);
|
||||
@@ -190,7 +190,7 @@ function encaixar(pecas,W){
|
||||
function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
const versao=area.montagemVersao=(area.montagemVersao||0)+1;
|
||||
const filme=larguraFilme(), PX_CM=W/filme;
|
||||
const itens=lista.map(a=>({...a}));
|
||||
const itens=lista.map((a,sourceIndex)=>({...a,sourceIndex}));
|
||||
if(fora) fora.innerHTML='';
|
||||
if(!itens.length){
|
||||
area.innerHTML='<div class="semmont"><b>A montagem aparece aqui</b>'+
|
||||
@@ -201,7 +201,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
|
||||
Promise.all(itens.map(a=>carregarImagem(a.f).then(img=>({a,img})))).then(imagens=>{
|
||||
if(area.montagemVersao!==versao) return;
|
||||
const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},()=>({...c})));
|
||||
const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},(_,copyIndex)=>({...c,copyIndex})));
|
||||
cargas.forEach(c=>{
|
||||
c.w = c.a.cm; // o motor trabalha em centímetros
|
||||
c.h = c.w * (c.img ? (c.a.giro? c.img.width/c.img.height : c.img.height/c.img.width) : 1);
|
||||
@@ -255,7 +255,12 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
+ (girou? ' · <b style="display:inline">'+girou+
|
||||
(girou>1?' artes giradas':' arte girada')+' 90°</b> para caber melhor':'');
|
||||
(fora||area).appendChild(el);
|
||||
if(aoTerminar) aoTerminar({alturaCm, uso, girou});
|
||||
if(aoTerminar) aoTerminar({alturaCm, uso, girou,
|
||||
placements:enc.pos.map(p=>({source_index:p.ref.a.sourceIndex,
|
||||
copy_index:p.ref.copyIndex,x_cm:p.x,y_cm:p.y,
|
||||
width_cm:+p.w.toFixed(4),length_cm:+p.h.toFixed(4),
|
||||
rotation_degrees:(p.rot+(p.ref.a.giro||0))%360,
|
||||
mirrored:!!p.ref.a.esp}))});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,6 +270,7 @@ function previaArtes(){ desenhaMontagem($('pArea'), artes, metros, 560); }
|
||||
let tMont=null;
|
||||
function previaAoVivo(){
|
||||
clearTimeout(tMont);
|
||||
montagemLayout=null;
|
||||
$('vArea').montagemVersao=($('vArea').montagemVersao||0)+1;
|
||||
if(ehFolha()){
|
||||
$('vUso').innerHTML='';
|
||||
@@ -296,11 +302,13 @@ function previaAoVivo(){
|
||||
return;
|
||||
}
|
||||
tMont=setTimeout(()=>{
|
||||
const prontas=artes.filter(a=>a.cm>0);
|
||||
const prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
|
||||
const W=Math.round(300*ZOOMS[zoomI]);
|
||||
$('vArea').classList.toggle('ampliado', zoomI>0);
|
||||
desenhaMontagem($('vArea'), prontas, 0, W, $('vUso'), r=>{
|
||||
montagemCm = r.alturaCm; // altura real da folha montada
|
||||
montagemLayout=r.placements ? {height_cm:+r.alturaCm.toFixed(4),
|
||||
placements:r.placements} : null;
|
||||
$('vMt').textContent = r.alturaCm? fmtM(cobrar(r.alturaCm/100))+' m' : '—';
|
||||
resumoArtes(); // o resumo lê a mesma folha
|
||||
agendaAvaliacao(); // a nota só sai com a metragem pronta
|
||||
@@ -312,12 +320,12 @@ function previaAoVivo(){
|
||||
function motivoNota(){
|
||||
if(!nota) return 'sem nota ainda · a conferência roda na sala antes de imprimir';
|
||||
if(!ehFolha()){
|
||||
const ruins=artes.filter(a=>a.cm>0 && (a.px/(a.cm/2.54))<300);
|
||||
const ruins=artes.filter(a=>a.cm>0 && dpiDe(a)<300);
|
||||
if(!ruins.length) return 'nota '+nota+' · todas as artes em 300 DPI ou mais';
|
||||
const pior=ruins.reduce((p,a)=>(a.px/(a.cm/2.54))<(p.px/(p.cm/2.54))?a:p);
|
||||
const pior=ruins.reduce((p,a)=>dpiDe(a)<dpiDe(p)?a:p);
|
||||
return 'nota '+nota+' · '+ruins.length+(ruins.length>1?' artes abaixo':' arte abaixo')+
|
||||
' de 300 DPI · a menor é "'+pior.f.name+'" com '+
|
||||
Math.round(pior.px/(pior.cm/2.54))+' DPI em '+pior.cm.toFixed(1).replace('.',',')+' cm';
|
||||
Math.round(dpiDe(pior))+' DPI em '+pior.cm.toFixed(1).replace('.',',')+' cm';
|
||||
}
|
||||
return nota>=90 ? 'nota '+nota+' · resolução ótima em toda a folha'
|
||||
: 'nota '+nota+' · a resolução é o único ponto — o resto corrigimos por você';
|
||||
@@ -350,4 +358,3 @@ function previa(){
|
||||
catch(e){ $('pArea').innerHTML='<div class="semprev"><b>Prévia indisponível</b>'+
|
||||
'O arquivo será processado normalmente.</div>'; }
|
||||
}
|
||||
|
||||
|
||||
189
web/site-pdf.js
189
web/site-pdf.js
@@ -10,9 +10,8 @@
|
||||
const PDFJS_URL='/vendor/pdf.min.js';
|
||||
const PDFJS_WORKER=new URL('/vendor/pdf.worker.min.js', location.origin).href;
|
||||
let pdfLibP=null, temWorker=null;
|
||||
// Alguns navegadores e iframes bloqueiam Worker de blob. Descobrimos antes de
|
||||
// tentar, porque sem worker o PDF é lido na thread principal e precisa de outra
|
||||
// estratégia: grade mais grossa e mais tempo.
|
||||
// Check whether this browser can create a same-origin Worker. The worker file
|
||||
// is served alongside this script and does not need a blob URL.
|
||||
function testaWorker(url){
|
||||
try{ const w=new Worker(url); w.terminate(); return true; }catch(e){ return false; }
|
||||
}
|
||||
@@ -25,10 +24,8 @@ function carregarPdfJs(){
|
||||
sc.src=PDFJS_URL;
|
||||
sc.onload=()=>{
|
||||
try{
|
||||
const codigo='importScripts("'+PDFJS_WORKER+'");';
|
||||
const url=URL.createObjectURL(new Blob([codigo],{type:'application/javascript'}));
|
||||
temWorker=testaWorker(url);
|
||||
window.pdfjsLib.GlobalWorkerOptions.workerSrc = temWorker? url : PDFJS_WORKER;
|
||||
temWorker=testaWorker(PDFJS_WORKER);
|
||||
window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER;
|
||||
}catch(e){
|
||||
temWorker=false;
|
||||
try{ window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER; }catch(e2){}
|
||||
@@ -61,7 +58,7 @@ function dpiDasImagens(page, ops){
|
||||
const larguraPt=Math.hypot(ctm[0],ctm[1]);
|
||||
if(larguraPt<1) continue;
|
||||
const areaPt=Math.abs(ctm[0]*ctm[3]-ctm[1]*ctm[2]);
|
||||
dpis.push({dpi: im.width/(larguraPt/72), area: areaPt});
|
||||
dpis.push({dpi: im.width/(larguraPt*page.userUnit/72), area: areaPt});
|
||||
}
|
||||
}
|
||||
return dpis;
|
||||
@@ -88,43 +85,58 @@ function resumoDpi(lista){
|
||||
}
|
||||
|
||||
// Rasteriza a primeira página a 1 mm por pixel e devolve a tela para a análise
|
||||
function rasterizarPdf(file, larguraCm, alturaCm){
|
||||
let motivo=null;
|
||||
const trabalho=carregarPdfJs().then(lib=>{
|
||||
if(!lib){ motivo='não foi possível carregar o leitor de PDF'; return null; }
|
||||
// sem worker tudo roda na thread principal · grade mais grossa para caber
|
||||
const folga = temWorker===false ? 2.2 : 1;
|
||||
return file.arrayBuffer().then(buf=>lib.getDocument({
|
||||
data:buf, disableFontFace:true, isEvalSupported:false, useSystemFonts:false,
|
||||
verbosity:0 // sem worker é fallback, não erro
|
||||
}).promise).then(doc=>
|
||||
doc.getPage(1).then(page=>{
|
||||
const vp1=page.getViewport({scale:1});
|
||||
const passo=passoDe(larguraCm, alturaCm||larguraCm)*folga;
|
||||
const alvo=Math.max(8, Math.round(larguraCm/passo));
|
||||
const esc=alvo/vp1.width;
|
||||
const vp=page.getViewport({scale:esc});
|
||||
const cv=document.createElement('canvas');
|
||||
cv.width=Math.max(8,Math.round(vp.width)); cv.height=Math.max(8,Math.round(vp.height));
|
||||
const ctx=cv.getContext('2d',{willReadFrequently:true});
|
||||
return page.render({canvasContext:ctx, viewport:vp, background:'rgba(0,0,0,0)'})
|
||||
.promise.then(()=>page.getOperatorList()).then(ops=>{
|
||||
let dpis=[];
|
||||
try{ dpis=dpiDasImagens(page,ops); }catch(e){}
|
||||
const r=resumoDpi(dpis);
|
||||
return {tela:cv, dpi: r? r.ponderado : null, res:r};
|
||||
});
|
||||
})
|
||||
);
|
||||
}).catch(e=>{ motivo=(e&&e.message)? e.message : 'erro ao abrir o PDF'; return null; });
|
||||
// sem worker o processo é lento: damos mais tempo antes de desistir
|
||||
const espera = temWorker===false ? 90000 : 30000;
|
||||
const tarde=new Promise(r=>setTimeout(()=>{ motivo='demorou demais neste navegador'; r('tempo'); }, espera));
|
||||
return Promise.race([trabalho, tarde]).then(r=>{
|
||||
const saida = r==='tempo'? null : r;
|
||||
if(!saida) return {erro: motivo||'não deu para conferir', semWorker: temWorker===false};
|
||||
return saida;
|
||||
async function rasterizarPdf(file, larguraCm, alturaCm){
|
||||
const lib=await carregarPdfJs();
|
||||
if(!lib) return {erro:'não foi possível carregar o leitor de PDF'};
|
||||
// Decide after the worker check; the old timeout always used 30 seconds.
|
||||
const espera=temWorker===false ? 90000 : 30000;
|
||||
const folga=temWorker===false ? 2.2 : 1;
|
||||
let loading=null, doc=null, rendering=null, expired=false, timer=null;
|
||||
const trabalho=(async()=>{
|
||||
try{
|
||||
const buf=await file.arrayBuffer();
|
||||
if(expired) return null;
|
||||
loading=lib.getDocument({data:buf, disableFontFace:true,
|
||||
isEvalSupported:false, useSystemFonts:false, verbosity:0});
|
||||
doc=await loading.promise;
|
||||
if(expired || doc.numPages!==1) return null;
|
||||
const page=await doc.getPage(1);
|
||||
if(expired) return null;
|
||||
const vp1=page.getViewport({scale:1});
|
||||
const passo=passoDe(larguraCm, alturaCm||larguraCm)*folga;
|
||||
const alvo=Math.max(8,Math.round(larguraCm/passo));
|
||||
const vp=page.getViewport({scale:alvo/vp1.width});
|
||||
const cv=document.createElement('canvas');
|
||||
cv.width=Math.max(8,Math.round(vp.width)); cv.height=Math.max(8,Math.round(vp.height));
|
||||
const ctx=cv.getContext('2d',{willReadFrequently:true});
|
||||
rendering=page.render({canvasContext:ctx, viewport:vp, background:'rgba(0,0,0,0)'});
|
||||
await rendering.promise;
|
||||
if(expired) return null;
|
||||
const ops=await page.getOperatorList();
|
||||
if(expired) return null;
|
||||
let dpis=[];
|
||||
try{ dpis=dpiDasImagens(page,ops); }catch(e){}
|
||||
const r=resumoDpi(dpis);
|
||||
return {tela:cv, dpi:r? r.ponderado:null, res:r};
|
||||
}catch(e){
|
||||
if(expired) return null;
|
||||
return {erro:(e&&e.message)||'erro ao abrir o PDF', semWorker:temWorker===false};
|
||||
}finally{
|
||||
if(doc) doc.destroy().catch(()=>{});
|
||||
else if(loading) loading.destroy().catch(()=>{});
|
||||
}
|
||||
})();
|
||||
const tardio=new Promise(resolve=>{
|
||||
timer=setTimeout(()=>{
|
||||
expired=true;
|
||||
try{ if(rendering) rendering.cancel(); }catch(e){}
|
||||
if(loading) loading.destroy().catch(()=>{});
|
||||
resolve({erro:'demorou demais neste navegador',semWorker:temWorker===false});
|
||||
},espera);
|
||||
});
|
||||
const result=await Promise.race([trabalho,tardio]);
|
||||
clearTimeout(timer);
|
||||
return result||{erro:'não deu para conferir',semWorker:temWorker===false};
|
||||
}
|
||||
|
||||
function medirFolha(file){
|
||||
@@ -143,29 +155,40 @@ function medirFolha(file){
|
||||
return;
|
||||
}
|
||||
if(/\.pdf$/.test(nome)){
|
||||
const pedaco=(inicio,fim)=>new Promise(r=>{
|
||||
const fr=new FileReader();
|
||||
fr.onload=()=>r(new TextDecoder('latin1').decode(new Uint8Array(fr.result)));
|
||||
fr.onerror=()=>r('');
|
||||
fr.readAsArrayBuffer(file.slice(inicio,fim));
|
||||
});
|
||||
const M=4*1048576;
|
||||
Promise.all([pedaco(0,M), pedaco(Math.max(0,file.size-M), file.size)]).then(([a,b])=>{
|
||||
const txt=a+b;
|
||||
const re=/\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)/;
|
||||
const m=txt.match(re);
|
||||
if(!m) return res(null);
|
||||
// UserUnit escala a página inteira · sem ele, 1 unidade = 1/72 pol
|
||||
const uu=txt.match(/\/UserUnit\s+([\d.]+)/);
|
||||
const esc=uu? Math.max(1,Math.min(75000,+uu[1])) : 1;
|
||||
const ptW=Math.abs(+m[3]-+m[1]), ptH=Math.abs(+m[4]-+m[2]);
|
||||
if(!(ptW>0&&ptH>0)) return res(null);
|
||||
const w=ptW*PT_CM*esc, h=ptH*PT_CM*esc;
|
||||
// acima de 14.400 unidades sem UserUnit o arquivo está fora da especificação
|
||||
const foraDoPadrao = !uu && (ptW>PDF_MAX_PT || ptH>PDF_MAX_PT);
|
||||
res({larg:+w.toFixed(1), alt:+h.toFixed(1),
|
||||
fonte:'página do PDF'+(uu? ' · UserUnit '+esc : ''), foraDoPadrao});
|
||||
});
|
||||
// PDF boxes can be inherited, compressed, rotated, and scaled by UserUnit.
|
||||
// Searching raw bytes for /MediaBox can read the wrong object or miss it.
|
||||
let doc=null;
|
||||
carregarPdfJs().then(async lib=>{
|
||||
if(!lib) throw new Error('Não foi possível carregar o leitor de PDF.');
|
||||
doc=await lib.getDocument({data:await file.arrayBuffer(),
|
||||
disableFontFace:true, isEvalSupported:false, useSystemFonts:false,
|
||||
verbosity:0}).promise;
|
||||
if(doc.numPages!==1)
|
||||
return {rejected:true, reason:'O PDF tem '+doc.numPages+
|
||||
' páginas. Envie cada folha como um PDF de uma página para calcular o preço correto.'};
|
||||
const page=await doc.getPage(1);
|
||||
const view=page.view, unit=page.userUnit, vp=page.getViewport({scale:1});
|
||||
if(!Array.isArray(view) || view.length!==4 ||
|
||||
!Number.isFinite(unit) || unit<=0 ||
|
||||
!Number.isFinite(vp.width) || !Number.isFinite(vp.height) ||
|
||||
vp.width<=0 || vp.height<=0)
|
||||
return {rejected:true, reason:'Não conseguimos determinar o tamanho desta página PDF. Exporte-a novamente.'};
|
||||
const ptW=Math.abs(view[2]-view[0]), ptH=Math.abs(view[3]-view[1]);
|
||||
if(!(ptW>0 && ptH>0) ||
|
||||
(unit===1 && (ptW>PDF_MAX_PT || ptH>PDF_MAX_PT)))
|
||||
return {rejected:true, reason:'A página passa do limite de 508 cm sem UserUnit. Divida a folha ou exporte em PNG.'};
|
||||
// This pinned PDF.js build exposes the page's rotated view in points;
|
||||
// userUnit is separate and must scale both physical dimensions.
|
||||
const w=vp.width*unit*PT_CM, h=vp.height*unit*PT_CM;
|
||||
if(!Number.isFinite(w) || !Number.isFinite(h) || w<=0 || h<=0 || h>6000)
|
||||
return {rejected:true, reason:'O tamanho desta página PDF não é suportado. Divida a folha em partes menores.'};
|
||||
// Keep precise geometry for the fit and quote; the UI rounds only
|
||||
// when displaying dimensions.
|
||||
return {larg:+w.toFixed(3), alt:+h.toFixed(3),
|
||||
fonte:'página do PDF'+(unit!==1 ? ' · UserUnit '+unit : '')};
|
||||
}).then(res).catch(()=>res({rejected:true,
|
||||
reason:'Não conseguimos ler este PDF. Exporte-o novamente como PDF de uma página ou PNG.'}))
|
||||
.finally(()=>{ if(doc) doc.destroy().catch(()=>{}); });
|
||||
return;
|
||||
}
|
||||
res(null);
|
||||
@@ -173,12 +196,13 @@ function medirFolha(file){
|
||||
}
|
||||
|
||||
function pintaFolha(){
|
||||
invalidaItemAtual();
|
||||
const L=larguraFilme();
|
||||
pintaTipoEnvio(); // trava o seletor enquanto houver folha
|
||||
if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; }
|
||||
|
||||
$('lista').innerHTML = folhas.map((x,i)=>{
|
||||
const lido=!!x.med, larga=lido && x.med.larg>L+0.5, suspeito=lido && x.med.foraDoPadrao;
|
||||
const lido=!!x.med, larga=lido && x.med.larg>L, suspeito=lido && x.med.foraDoPadrao;
|
||||
const sub=(x.m||0)*(x.rep||1);
|
||||
let medida='';
|
||||
if(lido){
|
||||
@@ -210,6 +234,9 @@ function pintaFolha(){
|
||||
? ' Ela cabe no <b>DTF têxtil de 57 cm</b> — troque de produto ali em cima.'
|
||||
: ' Remonte a folha em '+n1(L)+' cm e suba de novo.')+'</em>':'')+
|
||||
'</div>';
|
||||
}else if(x.measurementError){
|
||||
medida='<div class="med alerta"><b>Este PDF não pode ser orçado</b><span>'+
|
||||
escapeHTML(x.measurementError)+'</span></div>';
|
||||
}else{
|
||||
const teto=TABELA[modo], piso=pisoEscada();
|
||||
const dif=(x.m>0)? (teto-piso)*cobrar(x.m*(x.rep||1)) : 0;
|
||||
@@ -231,7 +258,7 @@ function pintaFolha(){
|
||||
'<span class="progT">'+(x.pct<50?'lendo o arquivo…':
|
||||
x.pct<70?'medindo a folha…':'conferindo a arte…')+'</span>'
|
||||
: x.semAnalise
|
||||
? '<div class="progF"><b>Não conseguimos conferir esta folha.</b> '+x.semAnalise+
|
||||
? '<div class="progF"><b>Não conseguimos conferir esta folha.</b> '+escapeHTML(x.semAnalise)+
|
||||
'. Seguimos com o preço de tabela — se quiser a nota e o desconto, exporte a mesma '+
|
||||
'folha em <b>PNG</b>, que a conferência é bem mais leve.</div>'
|
||||
: '';
|
||||
@@ -271,6 +298,8 @@ function pintaFolha(){
|
||||
$('lista').querySelectorAll('[data-comp]').forEach(el=>el.addEventListener('change',e=>{
|
||||
folhas[+el.dataset.comp].m=Math.max(0,Math.min(60,+e.target.value||0)); pintaFolha();
|
||||
}));
|
||||
$('lista').querySelectorAll('[data-repf], [data-comp]').forEach(el=>
|
||||
el.addEventListener('input',invalidaItemAtual));
|
||||
agendaAvaliacao(); previaAoVivo();
|
||||
}
|
||||
|
||||
@@ -323,10 +352,15 @@ function vizPeca(a){
|
||||
const n1=v=>v.toFixed(1).replace('.',',');
|
||||
|
||||
function pintaArtes(){
|
||||
invalidaItemAtual();
|
||||
$('lista').innerHTML=artes.map((a,i)=>{
|
||||
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
|
||||
if(a.cm>0){
|
||||
const dpi=Math.round(a.px/(a.cm/2.54)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
|
||||
if(a.decodeError){
|
||||
calc='<span class="er">Não conseguimos ler esta imagem; exporte novamente em PNG ou JPG</span>';
|
||||
}else if(a.cm>larguraFilme()){
|
||||
calc='<span class="er">Largura maior que o filme de '+n1(larguraFilme())+' cm</span>';
|
||||
}else if(a.cm>0 && a.src){
|
||||
const dpi=Math.round(dpiDe(a)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
|
||||
barra='<div class="qb2"><i class="'+cls(n)+'" style="width:'+n+'%"></i></div>';
|
||||
calc='<span class="'+cls(n)+'"><b>'+n+'%</b> · '+dpi+' DPI</span><span>'+
|
||||
a.q+' × '+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</span>';
|
||||
@@ -341,7 +375,8 @@ function pintaArtes(){
|
||||
dica = vizPeca(a) + dica;
|
||||
}
|
||||
return '<div class="art'+(a===recemChegada?' nova':'')+'"><div class="l1"><b>'+escapeHTML(a.f.name)+'</b>'+
|
||||
'<span style="color:var(--fraco);font-size:10.5px">'+a.px+' px</span>'+
|
||||
'<span style="color:var(--fraco);font-size:10.5px">'+
|
||||
(a.px>0?a.px+' × '+a.py+' px':'a ler')+'</span>'+
|
||||
'<button data-rm="'+i+'">×</button></div><div class="cps">'+
|
||||
'<div><label>largura na peça</label><input type="number" min="1" max="'+larguraFilme()+
|
||||
'" placeholder="cm" data-cm="'+i+'" value="'+(a.cm||'')+'"></div>'+
|
||||
@@ -358,11 +393,12 @@ function pintaArtes(){
|
||||
}).join('');
|
||||
$('lista').querySelectorAll('[data-rm]').forEach(b=>b.addEventListener('click',()=>{artes.splice(+b.dataset.rm,1);pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-cm]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.cm].cm=+i.value||0;pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=Math.max(1,Math.floor(+i.value||1));pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=Math.max(1,Math.min(200,Math.floor(+i.value||1)));pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-cm], [data-q]').forEach(i=>i.addEventListener('input',()=>{
|
||||
const largura=i.hasAttribute('data-cm');
|
||||
const a=artes[+(largura?i.dataset.cm:i.dataset.q)];
|
||||
a[largura?'cm':'q']=largura?Math.max(0,+i.value||0):Math.max(1,Math.floor(+i.value||1));
|
||||
a[largura?'cm':'q']=largura?Math.max(0,+i.value||0):Math.max(1,Math.min(200,Math.floor(+i.value||1)));
|
||||
invalidaItemAtual();
|
||||
previaAoVivo();
|
||||
}));
|
||||
$('lista').querySelectorAll('[data-usar]').forEach(b=>b.addEventListener('click',()=>{
|
||||
@@ -381,10 +417,10 @@ function pintaArtes(){
|
||||
previaAoVivo();
|
||||
}
|
||||
function resumoArtes(){
|
||||
const r=$('rA'), prontas=artes.filter(a=>a.cm>0);
|
||||
const r=$('rA'), prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
|
||||
if(!prontas.length){ r.style.display='none'; return; }
|
||||
const m=montagemCm/100; // a mesma folha que aparece ao lado
|
||||
const notas=prontas.map(a=>Math.max(6,Math.min(100,Math.round((a.px/(a.cm/2.54))/300*100))));
|
||||
const notas=prontas.map(a=>Math.max(6,Math.min(100,Math.round(dpiDe(a)/300*100))));
|
||||
const media=Math.round(notas.reduce((s,n)=>s+n,0)/notas.length), pior=Math.min(...notas);
|
||||
const falta=artes.length-prontas.length;
|
||||
r.style.display='block'; r.className='resumoA on';
|
||||
@@ -397,4 +433,3 @@ function resumoArtes(){
|
||||
' m'+(cobrar(m)===MINIMO_M?' · mínimo':'')+'</b></div>':''):'')+
|
||||
(falta?'<div class="r" style="color:var(--laranja)"><span>faltam informar</span><b>'+falta+'</b></div>':'');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
function avaliar(){
|
||||
if(ehFolha()){
|
||||
if(!folhas.length) return {pronto:false, falta:'Arraste sua folha montada para começar.'};
|
||||
const larga=folhas.find(x=>x.med && x.med.larg > larguraFilme()+0.5);
|
||||
const pdfInvalido=folhas.find(x=>x.measurementError);
|
||||
if(pdfInvalido) return {pronto:false, falta:pdfInvalido.f.name+': '+pdfInvalido.measurementError};
|
||||
const larga=folhas.find(x=>x.med && x.med.larg > larguraFilme());
|
||||
if(larga) return {pronto:false, falta:'A folha '+larga.f.name+' tem '+n1(larga.med.larg)+
|
||||
' cm e não cabe no filme de '+n1(larguraFilme())+' cm.'};
|
||||
const semMedida=folhas.filter(x=>!(x.m>0)).length;
|
||||
@@ -17,12 +19,14 @@ function avaliar(){
|
||||
metros=folhaTotalM();
|
||||
reencaixado=false;
|
||||
const ans=folhas.map(x=>x.an).filter(Boolean);
|
||||
if(!ans.length){
|
||||
// sem análise possível (PDF, CDR, PSD): não inventa nota
|
||||
const exts=[...new Set(folhas.map(f=>extDe(f.f.name)))].join(', ');
|
||||
if(ans.length!==folhas.length){
|
||||
// A nota precisa cobrir cada folha faturada. Uma folha sem análise não
|
||||
// herda a nota/desconto de outra folha que o navegador conseguiu abrir.
|
||||
const ausentes=folhas.filter(f=>!f.an);
|
||||
const exts=[...new Set(ausentes.map(f=>extDe(f.f.name)))].join(', ');
|
||||
mostraNota([
|
||||
['av','Sem nota · '+exts+' não abre no navegador',
|
||||
'não temos como conferir a arte antes de imprimir'],
|
||||
['av','Sem nota · '+exts+' sem análise completa',
|
||||
ausentes.length+' de '+folhas.length+' folha(s) precisam de conferência manual'],
|
||||
['er','Metro pela tabela · '+rs(TABELA[modo]),
|
||||
'exportando em PNG ou PDF, cai até '+rs(pisoEscada())],
|
||||
['fix','Alguém abre seu arquivo','a conferência é feita na mão, na sala']
|
||||
@@ -58,13 +62,18 @@ function avaliar(){
|
||||
return {pronto:true};
|
||||
}
|
||||
if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'};
|
||||
if(artes.some(a=>a.decodeError || !a.src || !(a.px>0) || !(a.py>0)))
|
||||
return {pronto:false, falta:'Não conseguimos ler uma das imagens. Exporte-a novamente em PNG ou JPG antes de continuar.'};
|
||||
if(artes.some(a=>!Number.isFinite(a.cm) || a.cm>larguraFilme()))
|
||||
return {pronto:false, falta:'A largura da arte precisa caber no filme de '+
|
||||
n1(larguraFilme())+' cm. Reduza a medida solicitada antes de continuar.'};
|
||||
const sem=artes.filter(a=>!a.cm).length;
|
||||
if(sem) return {pronto:false, falta:'Informe a largura de '+sem+' arte'+(sem>1?'s':'')+
|
||||
' para ver a nota e o preço.'};
|
||||
// metragem = altura da folha depois de montada, com os 5 mm entre peças
|
||||
metros=+(montagemCm/100).toFixed(4);
|
||||
if(!(metros>0)) return {pronto:false, falta:'Montando a folha…'};
|
||||
const notas=artes.map(a=>Math.max(6,Math.min(100,Math.round((a.px/(a.cm/2.54))/300*100))));
|
||||
const notas=artes.map(a=>Math.max(6,Math.min(100,Math.round(dpiDe(a)/300*100))));
|
||||
const media=Math.round(notas.reduce((s,n)=>s+n,0)/notas.length), pior=Math.min(...notas);
|
||||
mostraNota([
|
||||
['ok','Montagem por nossa conta','5 mm garantidos'],
|
||||
@@ -182,5 +191,8 @@ function ressalvaResolucao(){
|
||||
$('cienteOk').addEventListener('change',e=>{
|
||||
if($('ciente').classList.contains('recusa')) return; // recusa não se aceita
|
||||
$('qOk').disabled=!e.target.checked;
|
||||
if(itemAtual && itemAtual.qualityStatus==='warning'){
|
||||
itemAtual.qualityAcknowledged=e.target.checked;
|
||||
pintaEntrega();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,16 @@ function sel(fs){
|
||||
const fora = fs.filter(f=>!regra.test(f.name));
|
||||
fs = fs.filter(f=>regra.test(f.name));
|
||||
recusa(fora);
|
||||
const max=window.dtfUploadMaxBytes||128*1048576;
|
||||
const grandes=fs.filter(f=>f.size>max);
|
||||
if(grandes.length){
|
||||
const el=$('recusa');
|
||||
el.style.display='block';
|
||||
el.innerHTML='<b>Arquivo acima do limite de '+(max/1048576).toFixed(0)+
|
||||
' MB.</b> A verificação de segurança ainda não consegue liberar arquivos maiores. '+
|
||||
'Divida ou compacte a arte antes de enviar.';
|
||||
fs=fs.filter(f=>f.size<=max);
|
||||
}
|
||||
if(!fs.length) return;
|
||||
if(ehFolha()){
|
||||
const auto=fs.filter(f=>AUTO.test(f.name)).length;
|
||||
@@ -60,6 +70,9 @@ function sel(fs){
|
||||
novas.forEach(x=>{
|
||||
x.pct=5; pintaFolha();
|
||||
medirFolha(x.f).then(md=>{
|
||||
if(md && md.rejected){
|
||||
x.measurementError=md.reason; x.pct=null; pintaFolha(); return;
|
||||
}
|
||||
if(md && md.dpiFolha!=null && md.dpiFolha<DPI_RECUSA) return recusaFolha(x, md);
|
||||
x.med=md; if(md) x.m=md.alt/100; x.pct=55; pintaFolha();
|
||||
if(md && RENDERIZA.test(x.f.name)){
|
||||
@@ -93,7 +106,7 @@ function sel(fs){
|
||||
});
|
||||
return;
|
||||
}
|
||||
const novos=fs.map(f=>({f,px:px(f),cm:0,q:1,prop:1,giro:0,esp:false}));
|
||||
const novos=fs.map(f=>({f,px:0,py:0,cm:0,q:1,prop:1,giro:0,esp:false}));
|
||||
artes=novos.concat(artes); // a mais recente fica no topo da fila
|
||||
recemChegada=novos[0];
|
||||
pintaArtes();
|
||||
@@ -102,11 +115,12 @@ function sel(fs){
|
||||
}
|
||||
function medir(a){
|
||||
return carregarImagem(a.f).then(img=>{
|
||||
if(img){ a.prop=img.height/img.width; a.px=img.width; a.src=img.src; } else a.prop=a.prop||1;
|
||||
if(img){ a.prop=img.height/img.width; a.px=img.width; a.py=img.height; a.src=img.src; }
|
||||
else a.decodeError=true;
|
||||
return a;
|
||||
});
|
||||
}
|
||||
// A metragem sai do arquivo, nunca do peso em bytes.
|
||||
// imagem → proporção da imagem aplicada à largura do filme
|
||||
// PDF → MediaBox da primeira página, em pontos (1 pt = 1/72 pol)
|
||||
// PDF → página única lida pelo PDF.js, com boxes, rotação e UserUnit
|
||||
// TIFF, PSD, AI, CDR → o navegador não abre; o cliente informa o comprimento
|
||||
|
||||
Reference in New Issue
Block a user