feat: Advanced color palette extraction algorithm
- Build complete color palette instead of single dominant color - Filter out whites (RGB > 240), blacks (RGB < 15), and grays (saturation < 0.15) - Calculate HSL values for better color analysis - Quantize colors to group similar shades together - Score colors based on: * Saturation (prefer vibrant colors) * Frequency (weight by occurrence) * Optimal lightness (0.4-0.6 range preferred) - Select best scoring color from palette - Skip too light (> 0.85) or too dark (< 0.20) colors - Improved sampling rate (every 4 pixels instead of 10) - Console logging for debugging extracted palette - Fallback to emerald green if no suitable colors found
This commit is contained in:
652
shop-mode.html
652
shop-mode.html
@@ -735,17 +735,18 @@
|
|||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageData = ctx.getImageData(
|
const imgData = ctx.getImageData(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
canvas.width,
|
canvas.width,
|
||||||
canvas.height,
|
canvas.height,
|
||||||
);
|
);
|
||||||
const data = imageData.data;
|
const data = imgData.data;
|
||||||
|
|
||||||
// Sample colors (every 10 pixels for performance)
|
// Collect color palette with counts
|
||||||
const colorMap = {};
|
const colorMap = {};
|
||||||
for (let i = 0; i < data.length; i += 40) {
|
for (let i = 0; i < data.length; i += 16) {
|
||||||
|
// Sample every 4 pixels for better coverage
|
||||||
const r = data[i];
|
const r = data[i];
|
||||||
const g = data[i + 1];
|
const g = data[i + 1];
|
||||||
const b = data[i + 2];
|
const b = data[i + 2];
|
||||||
@@ -754,32 +755,93 @@
|
|||||||
// Skip transparent pixels
|
// Skip transparent pixels
|
||||||
if (a < 125) continue;
|
if (a < 125) continue;
|
||||||
|
|
||||||
// Skip very light colors (likely background)
|
// Skip whites (> 240 in all channels)
|
||||||
if (r > 240 && g > 240 && b > 240) continue;
|
if (r > 240 && g > 240 && b > 240) continue;
|
||||||
|
|
||||||
const rgb = `${r},${g},${b}`;
|
// Skip blacks (< 15 in all channels)
|
||||||
|
if (r < 15 && g < 15 && b < 15) continue;
|
||||||
|
|
||||||
|
// Skip grays (low saturation: when R≈G≈B)
|
||||||
|
const maxChan = Math.max(r, g, b);
|
||||||
|
const minChan = Math.min(r, g, b);
|
||||||
|
const saturation =
|
||||||
|
maxChan === 0
|
||||||
|
? 0
|
||||||
|
: (maxChan - minChan) / maxChan;
|
||||||
|
if (saturation < 0.15) continue; // Skip low saturation (grays)
|
||||||
|
|
||||||
|
// Quantize colors to reduce variations (group similar colors)
|
||||||
|
const qr = Math.round(r / 10) * 10;
|
||||||
|
const qg = Math.round(g / 10) * 10;
|
||||||
|
const qb = Math.round(b / 10) * 10;
|
||||||
|
const rgb = `${qr},${qg},${qb}`;
|
||||||
colorMap[rgb] = (colorMap[rgb] || 0) + 1;
|
colorMap[rgb] = (colorMap[rgb] || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find most common color
|
// Build palette with color scores
|
||||||
let maxCount = 0;
|
const palette = [];
|
||||||
let dominantColor = null;
|
|
||||||
for (const [rgb, count] of Object.entries(
|
for (const [rgb, count] of Object.entries(
|
||||||
colorMap,
|
colorMap,
|
||||||
)) {
|
)) {
|
||||||
if (count > maxCount) {
|
const [r, g, b] = rgb
|
||||||
maxCount = count;
|
|
||||||
dominantColor = rgb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dominantColor) {
|
|
||||||
const [r, g, b] = dominantColor
|
|
||||||
.split(",")
|
.split(",")
|
||||||
.map(Number);
|
.map(Number);
|
||||||
this.logoColor = this.rgbToHex(r, g, b);
|
|
||||||
|
// Calculate HSL for better color selection
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const l = (max + min) / 2 / 255; // Lightness 0-1
|
||||||
|
const s =
|
||||||
|
max === min
|
||||||
|
? 0
|
||||||
|
: (max - min) / (max + min); // Saturation 0-1
|
||||||
|
|
||||||
|
// Skip too light (> 0.85) or too dark (< 0.20)
|
||||||
|
if (l > 0.85 || l < 0.2) continue;
|
||||||
|
|
||||||
|
// Calculate color "score" based on:
|
||||||
|
// - Saturation (higher is better)
|
||||||
|
// - Frequency (more common is better)
|
||||||
|
// - Ideal lightness (0.4-0.6 is best)
|
||||||
|
const lightnessScore =
|
||||||
|
1 - Math.abs(l - 0.5) * 2; // Peaks at 0.5
|
||||||
|
const score =
|
||||||
|
s * 2 + count / 100 + lightnessScore;
|
||||||
|
|
||||||
|
palette.push({
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
count,
|
||||||
|
saturation: s,
|
||||||
|
lightness: l,
|
||||||
|
score,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score (best colors first)
|
||||||
|
palette.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
// Get the best color from palette
|
||||||
|
if (palette.length > 0) {
|
||||||
|
const bestColor = palette[0];
|
||||||
|
this.logoColor = this.rgbToHex(
|
||||||
|
bestColor.r,
|
||||||
|
bestColor.g,
|
||||||
|
bestColor.b,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Extracted color:",
|
||||||
|
this.logoColor,
|
||||||
|
"from palette of",
|
||||||
|
palette.length,
|
||||||
|
"colors",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logoColor = "#10b981"; // Fallback emerald
|
this.logoColor = "#10b981"; // Fallback emerald
|
||||||
|
console.log(
|
||||||
|
"No suitable colors found, using fallback",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error extracting color:", err);
|
console.error("Error extracting color:", err);
|
||||||
@@ -915,277 +977,377 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
},
|
},
|
||||||
|
|
||||||
generaPDF() {
|
generaPDF() {
|
||||||
const { jsPDF } = window.jspdf;
|
const { jsPDF } = window.jspdf;
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF();
|
||||||
|
|
||||||
let yPos = 0;
|
let yPos = 0;
|
||||||
|
|
||||||
// Determine brand color (from logo or default)
|
// Determine brand color (from logo or default)
|
||||||
const brandColor = this.logoColor || '#10b981';
|
const brandColor = this.logoColor || "#10b981";
|
||||||
const rgb = this.hexToRgb(brandColor);
|
const rgb = this.hexToRgb(brandColor);
|
||||||
const lightRgb = {
|
const lightRgb = {
|
||||||
r: Math.min(255, rgb.r + 180),
|
r: Math.min(255, rgb.r + 180),
|
||||||
g: Math.min(255, rgb.g + 180),
|
g: Math.min(255, rgb.g + 180),
|
||||||
b: Math.min(255, rgb.b + 180)
|
b: Math.min(255, rgb.b + 180),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== HEADER SECTION with gradient effect =====
|
// ===== HEADER SECTION with gradient effect =====
|
||||||
doc.setFillColor(lightRgb.r, lightRgb.g, lightRgb.b);
|
doc.setFillColor(lightRgb.r, lightRgb.g, lightRgb.b);
|
||||||
doc.rect(0, 0, 210, 55, 'F');
|
doc.rect(0, 0, 210, 55, "F");
|
||||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||||
doc.rect(0, 35, 210, 20, 'F');
|
doc.rect(0, 35, 210, 20, "F");
|
||||||
|
|
||||||
yPos = 18;
|
yPos = 18;
|
||||||
|
|
||||||
// Logo in header with white card background
|
// Logo in header with white card background
|
||||||
if (this.venditore.logo) {
|
if (this.venditore.logo) {
|
||||||
try {
|
try {
|
||||||
doc.setFillColor(255, 255, 255);
|
doc.setFillColor(255, 255, 255);
|
||||||
doc.roundedRect(15, yPos - 8, 35, 35, 3, 3, 'F');
|
doc.roundedRect(
|
||||||
doc.setDrawColor(230, 230, 230);
|
15,
|
||||||
doc.setLineWidth(0.5);
|
yPos - 8,
|
||||||
doc.roundedRect(15, yPos - 8, 35, 35, 3, 3, 'S');
|
35,
|
||||||
doc.addImage(this.venditore.logo, 'PNG', 18, yPos - 5, 29, 29);
|
35,
|
||||||
} catch (err) {
|
3,
|
||||||
console.error('Error adding logo to PDF:', err);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Company name and title
|
// Company name and title
|
||||||
const xStart = this.venditore.logo ? 55 : 15;
|
const xStart = this.venditore.logo ? 55 : 15;
|
||||||
doc.setFontSize(26);
|
doc.setFontSize(26);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(60, 60, 60);
|
doc.setTextColor(60, 60, 60);
|
||||||
doc.text(this.venditore.nome || "Venditore", xStart, yPos);
|
doc.text(
|
||||||
|
this.venditore.nome || "Venditore",
|
||||||
|
xStart,
|
||||||
|
yPos,
|
||||||
|
);
|
||||||
|
|
||||||
doc.setFontSize(14);
|
doc.setFontSize(14);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(255, 255, 255);
|
doc.setTextColor(255, 255, 255);
|
||||||
doc.text("PREVENTIVO", xStart, yPos + 23);
|
doc.text("PREVENTIVO", xStart, yPos + 23);
|
||||||
|
|
||||||
// Date badge
|
// Date badge
|
||||||
const dateStr = new Date(this.cliente.data).toLocaleDateString('it-IT');
|
const dateStr = new Date(
|
||||||
doc.setFillColor(255, 255, 255);
|
this.cliente.data,
|
||||||
doc.roundedRect(155, yPos + 15, 40, 12, 2, 2, 'F');
|
).toLocaleDateString("it-IT");
|
||||||
doc.setFontSize(10);
|
doc.setFillColor(255, 255, 255);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.roundedRect(155, yPos + 15, 40, 12, 2, 2, "F");
|
||||||
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
doc.setFontSize(10);
|
||||||
doc.text(dateStr, 175, yPos + 22, { align: 'center' });
|
doc.setFont(undefined, "bold");
|
||||||
|
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
||||||
|
doc.text(dateStr, 175, yPos + 22, { align: "center" });
|
||||||
|
|
||||||
yPos = 65;
|
yPos = 65;
|
||||||
|
|
||||||
// ===== INFO CARDS SECTION =====
|
// ===== INFO CARDS SECTION =====
|
||||||
// Venditore card
|
// Venditore card
|
||||||
doc.setFillColor(250, 250, 250);
|
doc.setFillColor(250, 250, 250);
|
||||||
doc.roundedRect(15, yPos, 85, 28, 3, 3, 'F');
|
doc.roundedRect(15, yPos, 85, 28, 3, 3, "F");
|
||||||
doc.setDrawColor(220, 220, 220);
|
doc.setDrawColor(220, 220, 220);
|
||||||
doc.setLineWidth(0.5);
|
doc.setLineWidth(0.5);
|
||||||
doc.roundedRect(15, yPos, 85, 28, 3, 3, 'S');
|
doc.roundedRect(15, yPos, 85, 28, 3, 3, "S");
|
||||||
|
|
||||||
// Venditore header with brand color
|
// Venditore header with brand color
|
||||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||||
doc.roundedRect(15, yPos, 85, 8, 3, 3, 'F');
|
doc.roundedRect(15, yPos, 85, 8, 3, 3, "F");
|
||||||
doc.rect(15, yPos + 5, 85, 3, 'F');
|
doc.rect(15, yPos + 5, 85, 3, "F");
|
||||||
|
|
||||||
doc.setFontSize(9);
|
doc.setFontSize(9);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(255, 255, 255);
|
doc.setTextColor(255, 255, 255);
|
||||||
doc.text("VENDITORE", 57.5, yPos + 5.5, { align: 'center' });
|
doc.text("VENDITORE", 57.5, yPos + 5.5, {
|
||||||
|
align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
// Venditore data
|
// Venditore data
|
||||||
doc.setFontSize(8);
|
doc.setFontSize(8);
|
||||||
doc.setFont(undefined, 'normal');
|
doc.setFont(undefined, "normal");
|
||||||
doc.setTextColor(60, 60, 60);
|
doc.setTextColor(60, 60, 60);
|
||||||
doc.text(this.venditore.nome || '-', 18, yPos + 13);
|
doc.text(this.venditore.nome || "-", 18, yPos + 13);
|
||||||
doc.text(`P.IVA: ${this.venditore.piva || '-'}`, 18, yPos + 18);
|
doc.text(
|
||||||
doc.text(this.venditore.indirizzo || '-', 18, yPos + 23);
|
`P.IVA: ${this.venditore.piva || "-"}`,
|
||||||
|
18,
|
||||||
|
yPos + 18,
|
||||||
|
);
|
||||||
|
doc.text(
|
||||||
|
this.venditore.indirizzo || "-",
|
||||||
|
18,
|
||||||
|
yPos + 23,
|
||||||
|
);
|
||||||
|
|
||||||
// Cliente card
|
// Cliente card
|
||||||
doc.setFillColor(250, 250, 250);
|
doc.setFillColor(250, 250, 250);
|
||||||
doc.roundedRect(110, yPos, 85, 28, 3, 3, 'F');
|
doc.roundedRect(110, yPos, 85, 28, 3, 3, "F");
|
||||||
doc.setDrawColor(220, 220, 220);
|
doc.setDrawColor(220, 220, 220);
|
||||||
doc.setLineWidth(0.5);
|
doc.setLineWidth(0.5);
|
||||||
doc.roundedRect(110, yPos, 85, 28, 3, 3, 'S');
|
doc.roundedRect(110, yPos, 85, 28, 3, 3, "S");
|
||||||
|
|
||||||
// Cliente header with brand color
|
// Cliente header with brand color
|
||||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||||
doc.roundedRect(110, yPos, 85, 8, 3, 3, 'F');
|
doc.roundedRect(110, yPos, 85, 8, 3, 3, "F");
|
||||||
doc.rect(110, yPos + 5, 85, 3, 'F');
|
doc.rect(110, yPos + 5, 85, 3, "F");
|
||||||
|
|
||||||
doc.setFontSize(9);
|
doc.setFontSize(9);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(255, 255, 255);
|
doc.setTextColor(255, 255, 255);
|
||||||
doc.text("CLIENTE", 152.5, yPos + 5.5, { align: 'center' });
|
doc.text("CLIENTE", 152.5, yPos + 5.5, {
|
||||||
|
align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
// Cliente data
|
// Cliente data
|
||||||
doc.setFontSize(8);
|
doc.setFontSize(8);
|
||||||
doc.setFont(undefined, 'normal');
|
doc.setFont(undefined, "normal");
|
||||||
doc.setTextColor(60, 60, 60);
|
doc.setTextColor(60, 60, 60);
|
||||||
doc.text(this.cliente.nome || '-', 113, yPos + 13);
|
doc.text(this.cliente.nome || "-", 113, yPos + 13);
|
||||||
doc.text(this.cliente.email || '-', 113, yPos + 18);
|
doc.text(this.cliente.email || "-", 113, yPos + 18);
|
||||||
doc.text(this.cliente.telefono || '-', 113, yPos + 23);
|
doc.text(this.cliente.telefono || "-", 113, yPos + 23);
|
||||||
|
|
||||||
yPos += 38;
|
yPos += 38;
|
||||||
|
|
||||||
// ===== ITEMS TABLE =====
|
// ===== ITEMS TABLE =====
|
||||||
// Table header with gradient
|
// Table header with gradient
|
||||||
doc.setFillColor(lightRgb.r, lightRgb.g, lightRgb.b);
|
doc.setFillColor(lightRgb.r, lightRgb.g, lightRgb.b);
|
||||||
doc.roundedRect(15, yPos, 180, 10, 2, 2, 'F');
|
doc.roundedRect(15, yPos, 180, 10, 2, 2, "F");
|
||||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||||
doc.roundedRect(15, yPos, 180, 10, 2, 2, 'F');
|
doc.roundedRect(15, yPos, 180, 10, 2, 2, "F");
|
||||||
|
|
||||||
doc.setFontSize(8);
|
doc.setFontSize(8);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(255, 255, 255);
|
doc.setTextColor(255, 255, 255);
|
||||||
doc.text("ARTICOLO", 18, yPos + 6.5);
|
doc.text("ARTICOLO", 18, yPos + 6.5);
|
||||||
doc.text("Q.TÀ", 110, yPos + 6.5, { align: 'center' });
|
doc.text("Q.TÀ", 110, yPos + 6.5, { align: "center" });
|
||||||
doc.text("PREZZO", 130, yPos + 6.5, { align: 'center' });
|
doc.text("PREZZO", 130, yPos + 6.5, {
|
||||||
doc.text("SC%", 150, yPos + 6.5, { align: 'center' });
|
align: "center",
|
||||||
doc.text("IVA%", 165, yPos + 6.5, { align: 'center' });
|
});
|
||||||
doc.text("TOTALE", 188, yPos + 6.5, { align: 'right' });
|
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" });
|
||||||
|
|
||||||
yPos += 14;
|
yPos += 14;
|
||||||
|
|
||||||
// Items rows with alternating background
|
// Items rows with alternating background
|
||||||
let itemIndex = 0;
|
let itemIndex = 0;
|
||||||
this.items.forEach((item) => {
|
this.items.forEach((item) => {
|
||||||
if (yPos > 255) {
|
if (yPos > 255) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
yPos = 20;
|
yPos = 20;
|
||||||
itemIndex = 0;
|
itemIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alternating row background
|
// Alternating row background
|
||||||
if (itemIndex % 2 === 0) {
|
if (itemIndex % 2 === 0) {
|
||||||
doc.setFillColor(250, 250, 250);
|
doc.setFillColor(250, 250, 250);
|
||||||
doc.rect(15, yPos - 2, 180, 10, 'F');
|
doc.rect(15, yPos - 2, 180, 10, "F");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item data
|
// Item data
|
||||||
doc.setFontSize(8);
|
doc.setFontSize(8);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(60, 60, 60);
|
doc.setTextColor(60, 60, 60);
|
||||||
doc.text(item.name.substring(0, 45), 18, yPos + 2);
|
doc.text(item.name.substring(0, 45), 18, yPos + 2);
|
||||||
|
|
||||||
doc.setFont(undefined, 'normal');
|
doc.setFont(undefined, "normal");
|
||||||
doc.text(String(item.quantity), 110, yPos + 2, { align: 'center' });
|
doc.text(String(item.quantity), 110, yPos + 2, {
|
||||||
doc.text(`€${item.price.toFixed(2)}`, 130, yPos + 2, { align: 'center' });
|
align: "center",
|
||||||
doc.text(`${item.discount}%`, 150, yPos + 2, { align: 'center' });
|
});
|
||||||
doc.text(`${item.iva || 0}%`, 165, 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.setFont(undefined, "bold");
|
||||||
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
doc.setTextColor(rgb.r, rgb.g, rgb.b);
|
||||||
doc.text(`€${this.calculateItemTotal(item).toFixed(2)}`, 188, yPos + 2, { align: 'right' });
|
doc.text(
|
||||||
|
`€${this.calculateItemTotal(item).toFixed(2)}`,
|
||||||
|
188,
|
||||||
|
yPos + 2,
|
||||||
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
|
||||||
// SKU and description
|
// SKU and description
|
||||||
if (item.sku || item.description) {
|
if (item.sku || item.description) {
|
||||||
yPos += 5;
|
yPos += 5;
|
||||||
doc.setFontSize(7);
|
doc.setFontSize(7);
|
||||||
doc.setTextColor(120, 120, 120);
|
doc.setTextColor(120, 120, 120);
|
||||||
doc.setFont(undefined, 'italic');
|
doc.setFont(undefined, "italic");
|
||||||
|
|
||||||
if (item.sku) {
|
if (item.sku) {
|
||||||
doc.text(`SKU: ${item.sku}`, 18, yPos);
|
doc.text(`SKU: ${item.sku}`, 18, yPos);
|
||||||
yPos += 3.5;
|
yPos += 3.5;
|
||||||
}
|
}
|
||||||
if (item.description) {
|
if (item.description) {
|
||||||
const descLines = doc.splitTextToSize(item.description, 165);
|
const descLines = doc.splitTextToSize(
|
||||||
descLines.slice(0, 2).forEach(line => {
|
item.description,
|
||||||
doc.text(line, 18, yPos);
|
165,
|
||||||
yPos += 3.5;
|
);
|
||||||
});
|
descLines.slice(0, 2).forEach((line) => {
|
||||||
}
|
doc.text(line, 18, yPos);
|
||||||
yPos += 2;
|
yPos += 3.5;
|
||||||
} else {
|
});
|
||||||
yPos += 10;
|
}
|
||||||
}
|
yPos += 2;
|
||||||
|
} else {
|
||||||
|
yPos += 10;
|
||||||
|
}
|
||||||
|
|
||||||
// Separator line
|
// Separator line
|
||||||
doc.setDrawColor(230, 230, 230);
|
doc.setDrawColor(230, 230, 230);
|
||||||
doc.setLineWidth(0.3);
|
doc.setLineWidth(0.3);
|
||||||
doc.line(15, yPos - 1, 195, yPos - 1);
|
doc.line(15, yPos - 1, 195, yPos - 1);
|
||||||
|
|
||||||
itemIndex++;
|
itemIndex++;
|
||||||
});
|
});
|
||||||
|
|
||||||
yPos += 5;
|
yPos += 5;
|
||||||
|
|
||||||
// ===== TOTALS SECTION =====
|
// ===== TOTALS SECTION =====
|
||||||
// Summary box
|
// Summary box
|
||||||
doc.setFillColor(250, 250, 250);
|
doc.setFillColor(250, 250, 250);
|
||||||
doc.roundedRect(115, yPos, 80, 45, 3, 3, 'F');
|
doc.roundedRect(115, yPos, 80, 45, 3, 3, "F");
|
||||||
doc.setDrawColor(220, 220, 220);
|
doc.setDrawColor(220, 220, 220);
|
||||||
doc.setLineWidth(0.5);
|
doc.setLineWidth(0.5);
|
||||||
doc.roundedRect(115, yPos, 80, 45, 3, 3, 'S');
|
doc.roundedRect(115, yPos, 80, 45, 3, 3, "S");
|
||||||
|
|
||||||
let summaryY = yPos + 8;
|
let summaryY = yPos + 8;
|
||||||
|
|
||||||
doc.setFontSize(9);
|
doc.setFontSize(9);
|
||||||
doc.setFont(undefined, 'normal');
|
doc.setFont(undefined, "normal");
|
||||||
doc.setTextColor(80, 80, 80);
|
doc.setTextColor(80, 80, 80);
|
||||||
|
|
||||||
// Subtotal
|
// Subtotal
|
||||||
doc.text("Subtotale:", 120, summaryY);
|
doc.text("Subtotale:", 120, summaryY);
|
||||||
doc.text(this.formatCurrency(this.calculateSubtotal()), 190, summaryY, { align: 'right' });
|
doc.text(
|
||||||
summaryY += 6;
|
this.formatCurrency(this.calculateSubtotal()),
|
||||||
|
190,
|
||||||
|
summaryY,
|
||||||
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
summaryY += 6;
|
||||||
|
|
||||||
// Discount
|
// Discount
|
||||||
if (this.calculateTotalDiscount() > 0) {
|
if (this.calculateTotalDiscount() > 0) {
|
||||||
doc.text("Sconto:", 120, summaryY);
|
doc.text("Sconto:", 120, summaryY);
|
||||||
doc.setTextColor(220, 50, 50);
|
doc.setTextColor(220, 50, 50);
|
||||||
doc.text(`-${this.formatCurrency(this.calculateTotalDiscount())}`, 190, summaryY, { align: 'right' });
|
doc.text(
|
||||||
doc.setTextColor(80, 80, 80);
|
`-${this.formatCurrency(this.calculateTotalDiscount())}`,
|
||||||
summaryY += 6;
|
190,
|
||||||
}
|
summaryY,
|
||||||
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
doc.setTextColor(80, 80, 80);
|
||||||
|
summaryY += 6;
|
||||||
|
}
|
||||||
|
|
||||||
// Shipping
|
// Shipping
|
||||||
if (this.shippingCost > 0) {
|
if (this.shippingCost > 0) {
|
||||||
doc.text("Spedizione:", 120, summaryY);
|
doc.text("Spedizione:", 120, summaryY);
|
||||||
doc.text(this.formatCurrency(this.shippingCost), 190, summaryY, { align: 'right' });
|
doc.text(
|
||||||
summaryY += 6;
|
this.formatCurrency(this.shippingCost),
|
||||||
}
|
190,
|
||||||
|
summaryY,
|
||||||
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
summaryY += 6;
|
||||||
|
}
|
||||||
|
|
||||||
// Taxable amount
|
// Taxable amount
|
||||||
doc.text("Imponibile:", 120, summaryY);
|
doc.text("Imponibile:", 120, summaryY);
|
||||||
doc.text(this.formatCurrency(this.calculateTaxableAmount()), 190, summaryY, { align: 'right' });
|
doc.text(
|
||||||
summaryY += 6;
|
this.formatCurrency(this.calculateTaxableAmount()),
|
||||||
|
190,
|
||||||
|
summaryY,
|
||||||
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
summaryY += 6;
|
||||||
|
|
||||||
// VAT
|
// VAT
|
||||||
if (this.calculateTax() > 0) {
|
if (this.calculateTax() > 0) {
|
||||||
doc.text("IVA:", 120, summaryY);
|
doc.text("IVA:", 120, summaryY);
|
||||||
doc.text(this.formatCurrency(this.calculateTax()), 190, summaryY, { align: 'right' });
|
doc.text(
|
||||||
summaryY += 8;
|
this.formatCurrency(this.calculateTax()),
|
||||||
} else {
|
190,
|
||||||
summaryY += 8;
|
summaryY,
|
||||||
}
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
summaryY += 8;
|
||||||
|
} else {
|
||||||
|
summaryY += 8;
|
||||||
|
}
|
||||||
|
|
||||||
// Total with brand color background
|
// Total with brand color background
|
||||||
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
doc.setFillColor(rgb.r, rgb.g, rgb.b);
|
||||||
doc.roundedRect(115, summaryY - 4, 80, 10, 2, 2, 'F');
|
doc.roundedRect(115, summaryY - 4, 80, 10, 2, 2, "F");
|
||||||
|
|
||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setFont(undefined, 'bold');
|
doc.setFont(undefined, "bold");
|
||||||
doc.setTextColor(255, 255, 255);
|
doc.setTextColor(255, 255, 255);
|
||||||
doc.text("TOTALE:", 120, summaryY + 2.5);
|
doc.text("TOTALE:", 120, summaryY + 2.5);
|
||||||
doc.setFontSize(13);
|
doc.setFontSize(13);
|
||||||
doc.text(this.formatCurrency(this.calculateTotal()), 190, summaryY + 2.5, { align: 'right' });
|
doc.text(
|
||||||
|
this.formatCurrency(this.calculateTotal()),
|
||||||
|
190,
|
||||||
|
summaryY + 2.5,
|
||||||
|
{ align: "right" },
|
||||||
|
);
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
yPos += 55;
|
yPos += 55;
|
||||||
if (yPos < 270) {
|
if (yPos < 270) {
|
||||||
doc.setFontSize(7);
|
doc.setFontSize(7);
|
||||||
doc.setFont(undefined, 'italic');
|
doc.setFont(undefined, "italic");
|
||||||
doc.setTextColor(150, 150, 150);
|
doc.setTextColor(150, 150, 150);
|
||||||
doc.text(`Documento generato il ${new Date().toLocaleString('it-IT')}`, 105, 285, { align: 'center' });
|
doc.text(
|
||||||
}
|
`Documento generato il ${new Date().toLocaleString("it-IT")}`,
|
||||||
|
105,
|
||||||
|
285,
|
||||||
|
{ align: "center" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Save PDF
|
// Save PDF
|
||||||
const filename = `Preventivo_${this.cliente.nome || "Cliente"}_${new Date().toISOString().split("T")[0]}.pdf`;
|
const filename = `Preventivo_${this.cliente.nome || "Cliente"}_${new Date().toISOString().split("T")[0]}.pdf`;
|
||||||
doc.save(filename);
|
doc.save(filename);
|
||||||
|
|
||||||
this.showNotification("PDF generato con successo!", "success");
|
this.showNotification(
|
||||||
},
|
"PDF generato con successo!",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
},
|
||||||
salvaDati() {
|
salvaDati() {
|
||||||
const data = {
|
const data = {
|
||||||
venditore: this.venditore,
|
venditore: this.venditore,
|
||||||
|
|||||||
Reference in New Issue
Block a user