From ea504d7b372a253aebb268333788adb677312795 Mon Sep 17 00:00:00 2001 From: "d.viti" Date: Tue, 14 Oct 2025 00:12:07 +0200 Subject: [PATCH] Add logo upload and color extraction for vendor - Allow uploading a company logo and extract its dominant color - Show logo preview and extracted color in the UI - Use logo and color for PDF branding and styling - Add per-item IVA field and calculation - Update PDF export to show logo, brand color, and IVA per item --- shop-mode.html | 315 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 284 insertions(+), 31 deletions(-) diff --git a/shop-mode.html b/shop-mode.html index 534df60..246f810 100644 --- a/shop-mode.html +++ b/shop-mode.html @@ -132,6 +132,60 @@ Dati Venditore + + +
+ +
+
+ Logo +
+
+ +

+ PNG, JPG, SVG fino a 2MB +

+
+ +
+
+ Colore estratto: +
+ +
+
+
+
+ + +
Prezzo scontato: + >Totale (IVA incl.):
@@ -603,8 +671,11 @@ piva: "", indirizzo: "", telefono: "", + logo: "", }, + logoColor: null, + cliente: { nome: "", email: "", @@ -623,6 +694,127 @@ this.addItem(); }, + handleLogoUpload(event) { + const file = event.target.files[0]; + if (!file) return; + + // Check file size (max 2MB) + if (file.size > 2 * 1024 * 1024) { + this.showNotification( + "Il file è troppo grande. Max 2MB", + "error", + ); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + this.venditore.logo = e.target.result; + this.extractDominantColor(e.target.result); + this.showNotification( + "Logo caricato con successo!", + "success", + ); + }; + reader.onerror = () => { + this.showNotification( + "Errore nel caricamento del logo", + "error", + ); + }; + reader.readAsDataURL(file); + }, + + extractDominantColor(imageData) { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + try { + const imageData = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height, + ); + const data = imageData.data; + + // Sample colors (every 10 pixels for performance) + const colorMap = {}; + for (let i = 0; i < data.length; i += 40) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + + // Skip transparent pixels + if (a < 125) continue; + + // Skip very light colors (likely background) + if (r > 240 && g > 240 && b > 240) continue; + + const rgb = `${r},${g},${b}`; + colorMap[rgb] = (colorMap[rgb] || 0) + 1; + } + + // Find most common color + let maxCount = 0; + let dominantColor = null; + for (const [rgb, count] of Object.entries( + colorMap, + )) { + if (count > maxCount) { + maxCount = count; + dominantColor = rgb; + } + } + + if (dominantColor) { + const [r, g, b] = dominantColor + .split(",") + .map(Number); + this.logoColor = this.rgbToHex(r, g, b); + } else { + this.logoColor = "#10b981"; // Fallback emerald + } + } catch (err) { + console.error("Error extracting color:", err); + this.logoColor = "#10b981"; // Fallback emerald + } + }; + img.src = imageData; + }, + + rgbToHex(r, g, b) { + return ( + "#" + + [r, g, b] + .map((x) => { + const hex = x.toString(16); + return hex.length === 1 ? "0" + hex : hex; + }) + .join("") + ); + }, + + hexToRgb(hex) { + const result = + /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec( + hex, + ); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : { r: 16, g: 185, b: 129 }; // Fallback emerald + }, + addItem() { this.itemCounter++; this.items.push({ @@ -633,6 +825,7 @@ price: 0, quantity: 1, discount: 0, + iva: this.taxRate || 22, }); }, @@ -646,6 +839,13 @@ return subtotal - discount; }, + calculateItemTotal(item) { + const discountedTotal = + this.calculateDiscountedTotal(item); + const iva = discountedTotal * ((item.iva || 0) / 100); + return discountedTotal + iva; + }, + calculateSubtotal() { return this.items.reduce((sum, item) => { return sum + item.price * item.quantity; @@ -666,15 +866,22 @@ }, calculateTax() { - return ( - this.calculateTaxableAmount() * (this.taxRate / 100) - ); + // Calcola l'IVA sommando l'IVA di ogni item + return this.items.reduce((sum, item) => { + const discountedTotal = + this.calculateDiscountedTotal(item); + const iva = + discountedTotal * ((item.iva || 0) / 100); + return sum + iva; + }, 0); }, calculateTotal() { - return ( - this.calculateTaxableAmount() + this.calculateTax() - ); + // Somma tutti i totali degli item (già con IVA) + spese di spedizione + const itemsTotal = this.items.reduce((sum, item) => { + return sum + this.calculateItemTotal(item); + }, 0); + return itemsTotal + this.shippingCost; }, calculateTotalQuantity() { @@ -714,21 +921,54 @@ let yPos = 20; - // Header - doc.setFontSize(20); - doc.setTextColor(16, 185, 129); + // Determine brand color (from logo or default) + const brandColor = this.logoColor || "#10b981"; + const rgb = this.hexToRgb(brandColor); + + // Header with logo + if (this.venditore.logo) { + try { + doc.addImage( + this.venditore.logo, + "PNG", + 15, + yPos, + 30, + 30, + ); + } catch (err) { + console.error("Error adding logo to PDF:", err); + } + } + + // Company name + doc.setFontSize(22); + doc.setFont(undefined, "bold"); + doc.setTextColor(rgb.r, rgb.g, rgb.b); doc.text( this.venditore.nome || "Venditore", - 105, - yPos, - { align: "center" }, + this.venditore.logo ? 50 : 105, + yPos + 10, + { align: this.venditore.logo ? "left" : "center" }, ); - yPos += 10; - doc.setFontSize(14); + doc.setFontSize(16); + doc.setFont(undefined, "bold"); doc.setTextColor(100); - doc.text("PREVENTIVO", 105, yPos, { align: "center" }); - yPos += 15; + doc.text( + "PREVENTIVO", + this.venditore.logo ? 50 : 105, + yPos + 20, + { align: this.venditore.logo ? "left" : "center" }, + ); + + yPos += this.venditore.logo ? 40 : 30; + + // Decorative line with brand color + doc.setDrawColor(rgb.r, rgb.g, rgb.b); + doc.setLineWidth(0.5); + doc.line(20, yPos, 190, yPos); + yPos += 5; // Dati venditore e cliente doc.setFontSize(10); @@ -748,17 +988,20 @@ doc.text(this.cliente.telefono, 120, yPos); yPos += 10; - // Tabella articoli + // Tabella articoli con brand color doc.setFontSize(10); - doc.setFillColor(16, 185, 129); + doc.setFillColor(rgb.r, rgb.g, rgb.b); doc.rect(20, yPos, 170, 7, "F"); - doc.setTextColor(255); + doc.setTextColor(255, 255, 255); + doc.setFont(undefined, "bold"); doc.text("Articolo", 25, yPos + 5); - doc.text("Q.tà", 120, yPos + 5); - doc.text("Prezzo", 140, yPos + 5); - doc.text("Sconto", 160, yPos + 5); - doc.text("Totale", 175, yPos + 5); + doc.text("Q.tà", 110, yPos + 5); + doc.text("Prezzo", 130, yPos + 5); + doc.text("Sc%", 150, yPos + 5); + doc.text("IVA%", 165, yPos + 5); + doc.text("Totale", 180, yPos + 5); yPos += 10; + doc.setFont(undefined, "normal"); doc.setTextColor(0); this.items.forEach((item) => { @@ -774,12 +1017,13 @@ doc.setFont(undefined, "normal"); // Dati numerici - doc.text(String(item.quantity), 125, yPos); - doc.text(`€${item.price.toFixed(2)}`, 140, yPos); - doc.text(`${item.discount}%`, 163, yPos); + doc.text(String(item.quantity), 113, yPos); + doc.text(`€${item.price.toFixed(2)}`, 130, yPos); + doc.text(`${item.discount}%`, 153, yPos); + doc.text(`${item.iva || 0}%`, 168, yPos); doc.text( - `€${this.calculateDiscountedTotal(item).toFixed(2)}`, - 175, + `€${this.calculateItemTotal(item).toFixed(2)}`, + 180, yPos, ); yPos += 5; @@ -862,8 +1106,9 @@ yPos += 7; } - doc.setFontSize(12); + doc.setFontSize(14); doc.setFont(undefined, "bold"); + doc.setTextColor(rgb.r, rgb.g, rgb.b); doc.text("TOTALE:", 120, yPos); doc.text( this.formatCurrency(this.calculateTotal()), @@ -871,6 +1116,12 @@ yPos, ); + // Footer decorative line + yPos += 5; + doc.setDrawColor(rgb.r, rgb.g, rgb.b); + doc.setLineWidth(0.5); + doc.line(20, yPos, 190, yPos); + const filename = `Preventivo_${this.cliente.nome || "Cliente"}_${new Date().toISOString().split("T")[0]}.pdf`; doc.save(filename); @@ -888,6 +1139,7 @@ taxRate: this.taxRate, shippingCost: this.shippingCost, invoiceNotes: this.invoiceNotes, + logoColor: this.logoColor, timestamp: new Date().toISOString(), }; @@ -931,6 +1183,7 @@ this.taxRate = data.taxRate || 22; this.shippingCost = data.shippingCost || 0; this.invoiceNotes = data.invoiceNotes || ""; + this.logoColor = data.logoColor || null; this.showNotification( "Dati caricati con successo!", "success",