feat: Kanban shows each order's artwork, filters quotes and searches orders and quotes
All checks were successful
Build and deploy / Validate source (push) Successful in 1m25s
Build and deploy / Integration suite on a real stack (push) Successful in 3m16s
Build and deploy / Secret scan and release gate (push) Successful in 13s
Build and deploy / Publish images (push) Successful in 1m29s

The customer's browser sends the small picture it already makes of each file
(at most 300 KB, WebP/JPEG/PNG read from the bytes, own uploads only); it goes
when the file's bytes go. Board cards, quote rows, the order panel and the
quote detail show it, with the layout drawing as a second view and as the
fallback for files without a picture.

Cotações gets a customer filter (e-mail, CNPJ, WhatsApp) and product, layout
and resolution-warning chips, with the page bar always shown. The board adds
Entrega, Retirada and stalled-order chips. The top search now covers orders in
any stage and unpaid quotes; CNPJ and phone match by digits only when nothing
but digits and punctuation was typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-10-01 11:46:33 -03:00
parent b54c635cfc
commit 352590e63a
15 changed files with 547 additions and 52 deletions

View File

@@ -118,6 +118,22 @@
}
return e.promise;
}
// The Kanban shows each order's artwork from the small picture this page
// already made of it (site-upload.js). Never blocks the order: a picture that
// fails to go leaves the Kanban with the layout drawing it shows today.
const fotosEnviadas=new Set();
function enviaFoto(uid, foto) {
if (!uid || !foto || fotosEnviadas.has(uid)) return;
fotosEnviadas.add(uid);
try {
const [cabecalho, dados] = foto.split(',');
const bin = atob(dados), bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
fetch('/api/uploads/'+uid+'/thumbnail', {method:'PUT', body:bytes,
headers:{'Content-Type':cabecalho.slice(5).split(';')[0]}})
.then(r=>{ if (!r.ok) fotosEnviadas.delete(uid); }, ()=>fotosEnviadas.delete(uid));
} catch (e) { fotosEnviadas.delete(uid); }
}
// A failed upload is shown and waits for the customer, never retried behind
// their back: a file the check refused would fail again on every change.
const falhas=new Map();
@@ -265,7 +281,10 @@
for (const item of cart) {
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));
for (const [index,file] of item.localFiles.entries()) {
uploads.push(await upload(file));
enviaFoto(uploads[index], item.fotos?.[index]);
}
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,