feat: Modernized PDF design with web panel style and dynamic branding
- Added logo upload functionality with base64 encoding - Automatic dominant color extraction from logo - Complete PDF redesign with modern card-based layout - Gradient header with company logo and name - Information cards for vendor and client data - Table with alternating row backgrounds - Summary box with brand color highlights - Per-item IVA support in calculations and PDF - Dynamic accent colors based on logo - Responsive layout with rounded corners and shadows - Professional footer with generation timestamp
This commit is contained in:
435
shop-mode.html
435
shop-mode.html
@@ -915,222 +915,277 @@
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
generaPDF() {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF();
|
||||
generaPDF() {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF();
|
||||
|
||||
let yPos = 20;
|
||||
let yPos = 0;
|
||||
|
||||
// Determine brand color (from logo or default)
|
||||
const brandColor = this.logoColor || "#10b981";
|
||||
const rgb = this.hexToRgb(brandColor);
|
||||
// Determine brand color (from logo or default)
|
||||
const brandColor = this.logoColor || '#10b981';
|
||||
const rgb = this.hexToRgb(brandColor);
|
||||
const lightRgb = {
|
||||
r: Math.min(255, rgb.r + 180),
|
||||
g: Math.min(255, rgb.g + 180),
|
||||
b: Math.min(255, rgb.b + 180)
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// ===== HEADER SECTION with gradient effect =====
|
||||
doc.setFillColor(lightRgb.r, lightRgb.g, lightRgb.b);
|
||||
doc.rect(0, 0, 210, 55, 'F');
|
||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.rect(0, 35, 210, 20, 'F');
|
||||
|
||||
// Company name
|
||||
doc.setFontSize(22);
|
||||
doc.setFont(undefined, "bold");
|
||||
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.text(
|
||||
this.venditore.nome || "Venditore",
|
||||
this.venditore.logo ? 50 : 105,
|
||||
yPos + 10,
|
||||
{ align: this.venditore.logo ? "left" : "center" },
|
||||
);
|
||||
yPos = 18;
|
||||
|
||||
doc.setFontSize(16);
|
||||
doc.setFont(undefined, "bold");
|
||||
doc.setTextColor(100);
|
||||
doc.text(
|
||||
"PREVENTIVO",
|
||||
this.venditore.logo ? 50 : 105,
|
||||
yPos + 20,
|
||||
{ align: this.venditore.logo ? "left" : "center" },
|
||||
);
|
||||
// Logo in header with white card background
|
||||
if (this.venditore.logo) {
|
||||
try {
|
||||
doc.setFillColor(255, 255, 255);
|
||||
doc.roundedRect(15, yPos - 8, 35, 35, 3, 3, 'F');
|
||||
doc.setDrawColor(230, 230, 230);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.roundedRect(15, yPos - 8, 35, 35, 3, 3, 'S');
|
||||
doc.addImage(this.venditore.logo, 'PNG', 18, yPos - 5, 29, 29);
|
||||
} catch (err) {
|
||||
console.error('Error adding logo to PDF:', err);
|
||||
}
|
||||
}
|
||||
|
||||
yPos += this.venditore.logo ? 40 : 30;
|
||||
// Company name and title
|
||||
const xStart = this.venditore.logo ? 55 : 15;
|
||||
doc.setFontSize(26);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(60, 60, 60);
|
||||
doc.text(this.venditore.nome || "Venditore", xStart, yPos);
|
||||
|
||||
// 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;
|
||||
doc.setFontSize(14);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text("PREVENTIVO", xStart, yPos + 23);
|
||||
|
||||
// Dati venditore e cliente
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0);
|
||||
doc.text("VENDITORE:", 20, yPos);
|
||||
doc.text("CLIENTE:", 120, yPos);
|
||||
yPos += 5;
|
||||
// Date badge
|
||||
const dateStr = new Date(this.cliente.data).toLocaleDateString('it-IT');
|
||||
doc.setFillColor(255, 255, 255);
|
||||
doc.roundedRect(155, yPos + 15, 40, 12, 2, 2, 'F');
|
||||
doc.setFontSize(10);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.text(dateStr, 175, yPos + 22, { align: 'center' });
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.text(this.venditore.nome, 20, yPos);
|
||||
doc.text(this.cliente.nome, 120, yPos);
|
||||
yPos += 5;
|
||||
doc.text(`P.IVA: ${this.venditore.piva}`, 20, yPos);
|
||||
doc.text(this.cliente.email, 120, yPos);
|
||||
yPos += 5;
|
||||
doc.text(this.venditore.indirizzo, 20, yPos);
|
||||
doc.text(this.cliente.telefono, 120, yPos);
|
||||
yPos += 10;
|
||||
yPos = 65;
|
||||
|
||||
// Tabella articoli con brand color
|
||||
doc.setFontSize(10);
|
||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.rect(20, yPos, 170, 7, "F");
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFont(undefined, "bold");
|
||||
doc.text("Articolo", 25, 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");
|
||||
// ===== INFO CARDS SECTION =====
|
||||
// Venditore card
|
||||
doc.setFillColor(250, 250, 250);
|
||||
doc.roundedRect(15, yPos, 85, 28, 3, 3, 'F');
|
||||
doc.setDrawColor(220, 220, 220);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.roundedRect(15, yPos, 85, 28, 3, 3, 'S');
|
||||
|
||||
doc.setTextColor(0);
|
||||
this.items.forEach((item) => {
|
||||
if (yPos > 260) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
// Venditore header with brand color
|
||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.roundedRect(15, yPos, 85, 8, 3, 3, 'F');
|
||||
doc.rect(15, yPos + 5, 85, 3, 'F');
|
||||
|
||||
// Nome prodotto
|
||||
doc.setFontSize(9);
|
||||
doc.setFont(undefined, "bold");
|
||||
doc.text(item.name.substring(0, 50), 25, yPos);
|
||||
doc.setFont(undefined, "normal");
|
||||
doc.setFontSize(9);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text("VENDITORE", 57.5, yPos + 5.5, { align: 'center' });
|
||||
|
||||
// Dati numerici
|
||||
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.calculateItemTotal(item).toFixed(2)}`,
|
||||
180,
|
||||
yPos,
|
||||
);
|
||||
yPos += 5;
|
||||
// Venditore data
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.setTextColor(60, 60, 60);
|
||||
doc.text(this.venditore.nome || '-', 18, yPos + 13);
|
||||
doc.text(`P.IVA: ${this.venditore.piva || '-'}`, 18, yPos + 18);
|
||||
doc.text(this.venditore.indirizzo || '-', 18, yPos + 23);
|
||||
|
||||
// SKU e descrizione (se presenti)
|
||||
if (item.sku || item.description) {
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(100);
|
||||
if (item.sku) {
|
||||
doc.text(`SKU: ${item.sku}`, 25, yPos);
|
||||
yPos += 4;
|
||||
}
|
||||
if (item.description) {
|
||||
const descLines = doc.splitTextToSize(
|
||||
item.description,
|
||||
160,
|
||||
);
|
||||
descLines.slice(0, 2).forEach((line) => {
|
||||
doc.text(line, 25, yPos);
|
||||
yPos += 4;
|
||||
});
|
||||
}
|
||||
doc.setTextColor(0);
|
||||
doc.setFontSize(9);
|
||||
yPos += 2;
|
||||
} else {
|
||||
yPos += 5;
|
||||
}
|
||||
});
|
||||
// Cliente card
|
||||
doc.setFillColor(250, 250, 250);
|
||||
doc.roundedRect(110, yPos, 85, 28, 3, 3, 'F');
|
||||
doc.setDrawColor(220, 220, 220);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.roundedRect(110, yPos, 85, 28, 3, 3, 'S');
|
||||
|
||||
yPos += 5;
|
||||
// Cliente header with brand color
|
||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.roundedRect(110, yPos, 85, 8, 3, 3, 'F');
|
||||
doc.rect(110, yPos + 5, 85, 3, 'F');
|
||||
|
||||
// Totali
|
||||
doc.line(120, yPos, 190, yPos);
|
||||
yPos += 7;
|
||||
doc.setFontSize(9);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text("CLIENTE", 152.5, yPos + 5.5, { align: 'center' });
|
||||
|
||||
doc.text("Subtotale:", 120, yPos);
|
||||
doc.text(
|
||||
this.formatCurrency(this.calculateSubtotal()),
|
||||
175,
|
||||
yPos,
|
||||
);
|
||||
yPos += 7;
|
||||
// Cliente data
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.setTextColor(60, 60, 60);
|
||||
doc.text(this.cliente.nome || '-', 113, yPos + 13);
|
||||
doc.text(this.cliente.email || '-', 113, yPos + 18);
|
||||
doc.text(this.cliente.telefono || '-', 113, yPos + 23);
|
||||
|
||||
if (this.calculateTotalDiscount() > 0) {
|
||||
doc.text("Sconto:", 120, yPos);
|
||||
doc.text(
|
||||
`-${this.formatCurrency(this.calculateTotalDiscount())}`,
|
||||
175,
|
||||
yPos,
|
||||
);
|
||||
yPos += 7;
|
||||
}
|
||||
yPos += 38;
|
||||
|
||||
if (this.shippingCost > 0) {
|
||||
doc.text("Spedizione:", 120, yPos);
|
||||
doc.text(
|
||||
this.formatCurrency(this.shippingCost),
|
||||
175,
|
||||
yPos,
|
||||
);
|
||||
yPos += 7;
|
||||
}
|
||||
// ===== ITEMS TABLE =====
|
||||
// Table header with gradient
|
||||
doc.setFillColor(lightRgb.r, lightRgb.g, lightRgb.b);
|
||||
doc.roundedRect(15, yPos, 180, 10, 2, 2, 'F');
|
||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.roundedRect(15, yPos, 180, 10, 2, 2, 'F');
|
||||
|
||||
doc.text("Imponibile:", 120, yPos);
|
||||
doc.text(
|
||||
this.formatCurrency(this.calculateTaxableAmount()),
|
||||
175,
|
||||
yPos,
|
||||
);
|
||||
yPos += 7;
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text("ARTICOLO", 18, yPos + 6.5);
|
||||
doc.text("Q.TÀ", 110, yPos + 6.5, { align: 'center' });
|
||||
doc.text("PREZZO", 130, yPos + 6.5, { align: 'center' });
|
||||
doc.text("SC%", 150, yPos + 6.5, { align: 'center' });
|
||||
doc.text("IVA%", 165, yPos + 6.5, { align: 'center' });
|
||||
doc.text("TOTALE", 188, yPos + 6.5, { align: 'right' });
|
||||
|
||||
if (this.taxRate > 0) {
|
||||
doc.text(`IVA (${this.taxRate}%):`, 120, yPos);
|
||||
doc.text(
|
||||
this.formatCurrency(this.calculateTax()),
|
||||
175,
|
||||
yPos,
|
||||
);
|
||||
yPos += 7;
|
||||
}
|
||||
yPos += 14;
|
||||
|
||||
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()),
|
||||
175,
|
||||
yPos,
|
||||
);
|
||||
// Items rows with alternating background
|
||||
let itemIndex = 0;
|
||||
this.items.forEach((item) => {
|
||||
if (yPos > 255) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
// Footer decorative line
|
||||
yPos += 5;
|
||||
doc.setDrawColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.line(20, yPos, 190, yPos);
|
||||
// Alternating row background
|
||||
if (itemIndex % 2 === 0) {
|
||||
doc.setFillColor(250, 250, 250);
|
||||
doc.rect(15, yPos - 2, 180, 10, 'F');
|
||||
}
|
||||
|
||||
const filename = `Preventivo_${this.cliente.nome || "Cliente"}_${new Date().toISOString().split("T")[0]}.pdf`;
|
||||
doc.save(filename);
|
||||
// Item data
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(60, 60, 60);
|
||||
doc.text(item.name.substring(0, 45), 18, yPos + 2);
|
||||
|
||||
this.showNotification(
|
||||
"PDF generato con successo!",
|
||||
"success",
|
||||
);
|
||||
},
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.text(String(item.quantity), 110, yPos + 2, { align: 'center' });
|
||||
doc.text(`€${item.price.toFixed(2)}`, 130, yPos + 2, { align: 'center' });
|
||||
doc.text(`${item.discount}%`, 150, yPos + 2, { align: 'center' });
|
||||
doc.text(`${item.iva || 0}%`, 165, yPos + 2, { align: 'center' });
|
||||
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.text(`€${this.calculateItemTotal(item).toFixed(2)}`, 188, yPos + 2, { align: 'right' });
|
||||
|
||||
// SKU and description
|
||||
if (item.sku || item.description) {
|
||||
yPos += 5;
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(120, 120, 120);
|
||||
doc.setFont(undefined, 'italic');
|
||||
|
||||
if (item.sku) {
|
||||
doc.text(`SKU: ${item.sku}`, 18, yPos);
|
||||
yPos += 3.5;
|
||||
}
|
||||
if (item.description) {
|
||||
const descLines = doc.splitTextToSize(item.description, 165);
|
||||
descLines.slice(0, 2).forEach(line => {
|
||||
doc.text(line, 18, yPos);
|
||||
yPos += 3.5;
|
||||
});
|
||||
}
|
||||
yPos += 2;
|
||||
} else {
|
||||
yPos += 10;
|
||||
}
|
||||
|
||||
// Separator line
|
||||
doc.setDrawColor(230, 230, 230);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(15, yPos - 1, 195, yPos - 1);
|
||||
|
||||
itemIndex++;
|
||||
});
|
||||
|
||||
yPos += 5;
|
||||
|
||||
// ===== TOTALS SECTION =====
|
||||
// Summary box
|
||||
doc.setFillColor(250, 250, 250);
|
||||
doc.roundedRect(115, yPos, 80, 45, 3, 3, 'F');
|
||||
doc.setDrawColor(220, 220, 220);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.roundedRect(115, yPos, 80, 45, 3, 3, 'S');
|
||||
|
||||
let summaryY = yPos + 8;
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.setTextColor(80, 80, 80);
|
||||
|
||||
// Subtotal
|
||||
doc.text("Subtotale:", 120, summaryY);
|
||||
doc.text(this.formatCurrency(this.calculateSubtotal()), 190, summaryY, { align: 'right' });
|
||||
summaryY += 6;
|
||||
|
||||
// Discount
|
||||
if (this.calculateTotalDiscount() > 0) {
|
||||
doc.text("Sconto:", 120, summaryY);
|
||||
doc.setTextColor(220, 50, 50);
|
||||
doc.text(`-${this.formatCurrency(this.calculateTotalDiscount())}`, 190, summaryY, { align: 'right' });
|
||||
doc.setTextColor(80, 80, 80);
|
||||
summaryY += 6;
|
||||
}
|
||||
|
||||
// Shipping
|
||||
if (this.shippingCost > 0) {
|
||||
doc.text("Spedizione:", 120, summaryY);
|
||||
doc.text(this.formatCurrency(this.shippingCost), 190, summaryY, { align: 'right' });
|
||||
summaryY += 6;
|
||||
}
|
||||
|
||||
// Taxable amount
|
||||
doc.text("Imponibile:", 120, summaryY);
|
||||
doc.text(this.formatCurrency(this.calculateTaxableAmount()), 190, summaryY, { align: 'right' });
|
||||
summaryY += 6;
|
||||
|
||||
// VAT
|
||||
if (this.calculateTax() > 0) {
|
||||
doc.text("IVA:", 120, summaryY);
|
||||
doc.text(this.formatCurrency(this.calculateTax()), 190, summaryY, { align: 'right' });
|
||||
summaryY += 8;
|
||||
} else {
|
||||
summaryY += 8;
|
||||
}
|
||||
|
||||
// Total with brand color background
|
||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||
doc.roundedRect(115, summaryY - 4, 80, 10, 2, 2, 'F');
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text("TOTALE:", 120, summaryY + 2.5);
|
||||
doc.setFontSize(13);
|
||||
doc.text(this.formatCurrency(this.calculateTotal()), 190, summaryY + 2.5, { align: 'right' });
|
||||
|
||||
// Footer
|
||||
yPos += 55;
|
||||
if (yPos < 270) {
|
||||
doc.setFontSize(7);
|
||||
doc.setFont(undefined, 'italic');
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text(`Documento generato il ${new Date().toLocaleString('it-IT')}`, 105, 285, { align: 'center' });
|
||||
}
|
||||
|
||||
// Save PDF
|
||||
const filename = `Preventivo_${this.cliente.nome || "Cliente"}_${new Date().toISOString().split("T")[0]}.pdf`;
|
||||
doc.save(filename);
|
||||
|
||||
this.showNotification("PDF generato con successo!", "success");
|
||||
},
|
||||
salvaDati() {
|
||||
const data = {
|
||||
venditore: this.venditore,
|
||||
|
||||
Reference in New Issue
Block a user