Files
calcolatore_prezzi_software/shop-mode.html
d.viti ea504d7b37 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
2025-10-14 00:12:07 +02:00

1231 lines
55 KiB
HTML

<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calcolatore Prezzi - Modalità Negozio</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Alpine.js -->
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<!-- jsPDF per generazione PDF -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<!-- Font Awesome per icone -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<!-- Google Fonts -->
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: "Inter", sans-serif;
}
[x-cloak] {
display: none !important;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slide-in {
animation: slideIn 0.5s ease-out;
}
.glass {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border-radius: 10px;
}
</style>
</head>
<body
class="bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-500 min-h-screen"
x-data="shopApp()"
x-init="init()"
>
<div class="container mx-auto p-4 max-w-7xl">
<!-- Header -->
<div
class="glass rounded-2xl p-8 mb-6 animate-slide-in text-center"
>
<h1
class="text-4xl font-bold text-gray-800 flex items-center justify-center gap-3"
>
<i class="fas fa-store text-emerald-600"></i>
Calcolatore Prezzi - Modalità Negozio
</h1>
<p class="text-gray-600 mt-2">
Sistema di preventivi basato su articoli e quantità
</p>
</div>
<!-- Notifiche Toast -->
<div
x-show="notification.show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed top-4 right-4 z-50"
>
<div
:class="'p-4 rounded-lg shadow-lg flex items-center gap-3 ' +
(notification.type === 'success' ? 'bg-green-500' :
notification.type === 'error' ? 'bg-red-500' :
notification.type === 'warning' ? 'bg-orange-500' : 'bg-blue-500') +
' text-white'"
>
<i
:class="'fas fa-' +
(notification.type === 'success' ? 'check-circle' :
notification.type === 'error' ? 'exclamation-circle' :
notification.type === 'warning' ? 'exclamation-triangle' : 'info-circle')"
></i>
<span x-text="notification.message"></span>
</div>
</div>
<!-- Dati Venditore -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<h2
class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2"
>
<i class="fas fa-building text-emerald-600"></i>
Dati Venditore
</h2>
<!-- Logo Upload -->
<div
class="mb-6 p-4 bg-gradient-to-r from-emerald-50 to-teal-50 rounded-lg border-2 border-emerald-200"
>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-image text-emerald-600 mr-2"></i>
Logo Aziendale
</label>
<div class="flex items-center gap-4">
<div x-show="venditore.logo" class="flex-shrink-0">
<img
:src="venditore.logo"
alt="Logo"
class="h-20 w-20 object-contain border-2 border-emerald-300 rounded-lg bg-white p-2"
/>
</div>
<div class="flex-1">
<input
type="file"
@change="handleLogoUpload($event)"
accept="image/*"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-emerald-500 file:text-white hover:file:bg-emerald-600 file:cursor-pointer cursor-pointer"
/>
<p class="text-xs text-gray-500 mt-1">
PNG, JPG, SVG fino a 2MB
</p>
</div>
<button
x-show="venditore.logo"
@click="venditore.logo = ''; logoColor = null"
class="px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-trash"></i>
</button>
</div>
<div
x-show="logoColor"
class="mt-3 flex items-center gap-2"
>
<span class="text-xs text-gray-600"
>Colore estratto:</span
>
<div
:style="'background-color: ' + logoColor"
class="w-8 h-8 rounded border-2 border-gray-300"
></div>
<span
class="text-xs font-mono"
x-text="logoColor"
></span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input
type="text"
x-model="venditore.nome"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="Nome Azienda"
/>
<i
class="fas fa-signature absolute left-3 top-4 text-emerald-400"
></i>
</div>
<div class="relative">
<input
type="text"
x-model="venditore.piva"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="P.IVA"
/>
<i
class="fas fa-id-card absolute left-3 top-4 text-emerald-400"
></i>
</div>
<div class="relative">
<input
type="text"
x-model="venditore.indirizzo"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="Indirizzo"
/>
<i
class="fas fa-map-marker-alt absolute left-3 top-4 text-emerald-400"
></i>
</div>
<div class="relative">
<input
type="text"
x-model="venditore.telefono"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="Telefono"
/>
<i
class="fas fa-phone absolute left-3 top-4 text-emerald-400"
></i>
</div>
</div>
</div>
<!-- Dati Cliente -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<h2
class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2"
>
<i class="fas fa-user text-emerald-600"></i>
Dati Cliente
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input
type="text"
x-model="cliente.nome"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="Nome Cliente"
/>
<i
class="fas fa-user-tie absolute left-3 top-4 text-emerald-400"
></i>
</div>
<div class="relative">
<input
type="email"
x-model="cliente.email"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="Email"
/>
<i
class="fas fa-envelope absolute left-3 top-4 text-emerald-400"
></i>
</div>
<div class="relative">
<input
type="text"
x-model="cliente.telefono"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
placeholder="Telefono"
/>
<i
class="fas fa-phone absolute left-3 top-4 text-emerald-400"
></i>
</div>
<div class="relative">
<input
type="date"
x-model="cliente.data"
class="w-full p-3 pl-10 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none transition-all"
/>
<i
class="fas fa-calendar absolute left-3 top-4 text-emerald-400"
></i>
</div>
</div>
</div>
<!-- Catalogo Prodotti -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<h2
class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2"
>
<i class="fas fa-boxes text-emerald-600"></i>
Catalogo Articoli
</h2>
<div class="space-y-3 mb-4">
<template x-for="(item, index) in items" :key="item.id">
<div
class="p-4 bg-gradient-to-r from-emerald-50 to-teal-50 rounded-lg border-2 border-emerald-200"
>
<div class="flex items-start justify-between gap-4">
<div
class="flex-1 grid grid-cols-1 md:grid-cols-5 gap-3"
>
<div class="md:col-span-2">
<label
class="block text-xs font-medium text-gray-700 mb-1"
>Nome Articolo</label
>
<input
type="text"
x-model="item.name"
class="w-full p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
placeholder="es. Licenza Software XYZ"
/>
</div>
<div>
<label
class="block text-xs font-medium text-gray-700 mb-1"
>Codice SKU</label
>
<input
type="text"
x-model="item.sku"
class="w-full p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
placeholder="SKU-001"
/>
</div>
<div>
<label
class="block text-xs font-medium text-gray-700 mb-1"
>Prezzo Unitario (€)</label
>
<input
type="number"
x-model.number="item.price"
class="w-full p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
step="0.01"
min="0"
/>
</div>
<div>
<label
class="block text-xs font-medium text-gray-700 mb-1"
>Quantità</label
>
<input
type="number"
x-model.number="item.quantity"
class="w-full p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
step="1"
min="0"
/>
</div>
</div>
<div class="flex flex-col items-end gap-2">
<button
@click="removeItem(index)"
class="text-red-500 hover:text-red-700 transition-colors"
>
<i class="fas fa-trash"></i>
</button>
<div class="text-right">
<p class="text-xs text-gray-600">
Totale
</p>
<p
class="text-lg font-bold text-emerald-600"
x-text="'€' + (item.price * item.quantity).toFixed(2)"
></p>
</div>
</div>
</div>
<div class="mt-3">
<label
class="block text-xs font-medium text-gray-700 mb-1"
>Descrizione</label
>
<textarea
x-model="item.description"
class="w-full p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
rows="2"
placeholder="Descrizione dettagliata dell'articolo"
></textarea>
</div>
<div class="mt-3 flex items-center gap-4">
<div>
<label
class="block text-xs font-medium text-gray-700 mb-1"
>Sconto (%)</label
>
<input
type="number"
x-model.number="item.discount"
class="w-24 p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
step="1"
min="0"
max="100"
/>
</div>
<div>
<label
class="block text-xs font-medium text-gray-700 mb-1"
>IVA (%)</label
>
<input
type="number"
x-model.number="item.iva"
class="w-24 p-2 text-sm border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
step="0.01"
min="0"
max="100"
/>
</div>
<div class="flex-1 text-right">
<span class="text-xs text-gray-600"
>Totale (IVA incl.):
</span>
<span
class="font-bold text-emerald-600"
x-text="'€' + calculateItemTotal(item).toFixed(2)"
></span>
</div>
</div>
</div>
</template>
</div>
<button
@click="addItem()"
class="w-full bg-gradient-to-r from-emerald-500 to-teal-500 text-white font-bold py-3 px-6 rounded-lg hover:from-emerald-600 hover:to-teal-600 transition-all flex items-center justify-center gap-2"
>
<i class="fas fa-plus"></i>
Aggiungi Articolo
</button>
</div>
<!-- Configurazione Fattura -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<h2
class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2"
>
<i class="fas fa-cog text-emerald-600"></i>
Configurazione Fattura
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label
class="block text-sm font-medium text-gray-700 mb-2"
>IVA (%)</label
>
<input
type="number"
x-model.number="taxRate"
class="w-full p-3 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
placeholder="es. 22"
step="0.01"
min="0"
max="100"
/>
<p class="text-xs text-gray-500 mt-1">
Comuni: 0% (esente), 4%, 10%, 22%
</p>
</div>
<div>
<label
class="block text-sm font-medium text-gray-700 mb-2"
>Spese di Spedizione (€)</label
>
<input
type="number"
x-model.number="shippingCost"
class="w-full p-3 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
step="0.01"
min="0"
/>
</div>
<div class="md:col-span-2">
<label
class="block text-sm font-medium text-gray-700 mb-2"
>Note Fattura</label
>
<textarea
x-model="invoiceNotes"
class="w-full p-3 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none"
rows="3"
placeholder="Inserisci eventuali note o condizioni di pagamento"
></textarea>
</div>
</div>
</div>
<!-- Riepilogo Totali -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<h2
class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2"
>
<i class="fas fa-calculator text-emerald-600"></i>
Riepilogo Preventivo
</h2>
<div
class="bg-gradient-to-br from-emerald-50 to-teal-100 p-6 rounded-xl shadow-lg"
>
<div class="space-y-3">
<div
class="flex justify-between p-2 bg-white/70 rounded"
>
<span class="text-gray-700"
>Subtotale Articoli</span
>
<span
class="font-bold"
x-text="formatCurrency(calculateSubtotal())"
></span>
</div>
<div
class="flex justify-between p-2 bg-white/70 rounded"
x-show="calculateTotalDiscount() > 0"
>
<span class="text-orange-600">Sconto Totale</span>
<span
class="font-bold text-orange-600"
x-text="'- ' + formatCurrency(calculateTotalDiscount())"
></span>
</div>
<div
class="flex justify-between p-2 bg-white/70 rounded"
x-show="shippingCost > 0"
>
<span class="text-gray-700"
>Spese di Spedizione</span
>
<span
class="font-bold"
x-text="formatCurrency(shippingCost)"
></span>
</div>
<div
class="flex justify-between p-2 bg-white/70 rounded"
>
<span class="text-gray-700">Imponibile</span>
<span
class="font-bold"
x-text="formatCurrency(calculateTaxableAmount())"
></span>
</div>
<div
class="flex justify-between p-2 bg-white/70 rounded"
x-show="taxRate > 0"
>
<span
class="text-gray-700"
x-text="'IVA (' + taxRate + '%)'"
></span>
<span
class="font-bold"
x-text="formatCurrency(calculateTax())"
></span>
</div>
<div class="border-t-2 border-emerald-300 pt-3 mt-3">
<div class="flex justify-between items-center">
<span class="text-xl font-bold text-emerald-800"
>TOTALE</span
>
<span
class="text-3xl font-bold text-emerald-600"
x-text="formatCurrency(calculateTotal())"
></span>
</div>
</div>
</div>
</div>
<!-- Statistiche -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mt-6">
<div class="text-center p-4 bg-emerald-50 rounded-lg">
<i
class="fas fa-box text-2xl text-emerald-600 mb-2"
></i>
<p class="text-sm text-gray-600">Articoli</p>
<p
class="text-xl font-bold text-emerald-600"
x-text="items.length"
></p>
</div>
<div class="text-center p-4 bg-teal-50 rounded-lg">
<i
class="fas fa-layer-group text-2xl text-teal-600 mb-2"
></i>
<p class="text-sm text-gray-600">Quantità Totale</p>
<p
class="text-xl font-bold text-teal-600"
x-text="calculateTotalQuantity()"
></p>
</div>
<div class="text-center p-4 bg-cyan-50 rounded-lg">
<i
class="fas fa-percentage text-2xl text-cyan-600 mb-2"
></i>
<p class="text-sm text-gray-600">Sconto Medio</p>
<p
class="text-xl font-bold text-cyan-600"
x-text="calculateAverageDiscount().toFixed(1) + '%'"
></p>
</div>
</div>
</div>
<!-- Pulsanti Azione -->
<div class="glass rounded-2xl p-6 animate-slide-in">
<div class="flex flex-wrap gap-3 justify-center">
<button
@click="generaPDF()"
class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-all flex items-center gap-2"
>
<i class="fas fa-file-pdf"></i>
Genera PDF
</button>
<button
@click="salvaDati()"
class="bg-teal-500 hover:bg-teal-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-all flex items-center gap-2"
>
<i class="fas fa-save"></i>
Salva
</button>
<button
@click="caricaDati()"
class="bg-cyan-500 hover:bg-cyan-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-all flex items-center gap-2"
>
<i class="fas fa-upload"></i>
Carica
</button>
<button
@click="stampa()"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-all flex items-center gap-2"
>
<i class="fas fa-print"></i>
Stampa
</button>
<button
@click="resetForm()"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-all flex items-center gap-2"
>
<i class="fas fa-redo"></i>
Reset
</button>
</div>
</div>
</div>
<script>
function shopApp() {
return {
notification: {
show: false,
message: "",
type: "info",
},
venditore: {
nome: "",
piva: "",
indirizzo: "",
telefono: "",
logo: "",
},
logoColor: null,
cliente: {
nome: "",
email: "",
telefono: "",
data: new Date().toISOString().split("T")[0],
},
items: [],
itemCounter: 0,
taxRate: 22,
shippingCost: 0,
invoiceNotes: "",
init() {
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({
id: this.itemCounter,
name: "",
sku: "",
description: "",
price: 0,
quantity: 1,
discount: 0,
iva: this.taxRate || 22,
});
},
removeItem(index) {
this.items.splice(index, 1);
},
calculateDiscountedTotal(item) {
const subtotal = item.price * item.quantity;
const discount = subtotal * (item.discount / 100);
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;
}, 0);
},
calculateTotalDiscount() {
return this.items.reduce((sum, item) => {
const subtotal = item.price * item.quantity;
return sum + subtotal * (item.discount / 100);
}, 0);
},
calculateTaxableAmount() {
const subtotal = this.calculateSubtotal();
const discount = this.calculateTotalDiscount();
return subtotal - discount + this.shippingCost;
},
calculateTax() {
// 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() {
// 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() {
return this.items.reduce(
(sum, item) => sum + item.quantity,
0,
);
},
calculateAverageDiscount() {
if (this.items.length === 0) return 0;
const totalDiscount = this.items.reduce(
(sum, item) => sum + item.discount,
0,
);
return totalDiscount / this.items.length;
},
formatCurrency(value) {
return `${value.toFixed(2)}`;
},
showNotification(message, type = "info") {
this.notification = {
show: true,
message: message,
type: type,
};
setTimeout(() => {
this.notification.show = false;
}, 3000);
},
generaPDF() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
let yPos = 20;
// 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",
this.venditore.logo ? 50 : 105,
yPos + 10,
{ align: this.venditore.logo ? "left" : "center" },
);
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" },
);
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);
doc.setTextColor(0);
doc.text("VENDITORE:", 20, yPos);
doc.text("CLIENTE:", 120, yPos);
yPos += 5;
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;
// 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");
doc.setTextColor(0);
this.items.forEach((item) => {
if (yPos > 260) {
doc.addPage();
yPos = 20;
}
// Nome prodotto
doc.setFontSize(9);
doc.setFont(undefined, "bold");
doc.text(item.name.substring(0, 50), 25, yPos);
doc.setFont(undefined, "normal");
// 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;
// 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;
}
});
yPos += 5;
// Totali
doc.line(120, yPos, 190, yPos);
yPos += 7;
doc.text("Subtotale:", 120, yPos);
doc.text(
this.formatCurrency(this.calculateSubtotal()),
175,
yPos,
);
yPos += 7;
if (this.calculateTotalDiscount() > 0) {
doc.text("Sconto:", 120, yPos);
doc.text(
`-${this.formatCurrency(this.calculateTotalDiscount())}`,
175,
yPos,
);
yPos += 7;
}
if (this.shippingCost > 0) {
doc.text("Spedizione:", 120, yPos);
doc.text(
this.formatCurrency(this.shippingCost),
175,
yPos,
);
yPos += 7;
}
doc.text("Imponibile:", 120, yPos);
doc.text(
this.formatCurrency(this.calculateTaxableAmount()),
175,
yPos,
);
yPos += 7;
if (this.taxRate > 0) {
doc.text(`IVA (${this.taxRate}%):`, 120, yPos);
doc.text(
this.formatCurrency(this.calculateTax()),
175,
yPos,
);
yPos += 7;
}
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,
);
// 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);
this.showNotification(
"PDF generato con successo!",
"success",
);
},
salvaDati() {
const data = {
venditore: this.venditore,
cliente: this.cliente,
items: this.items,
taxRate: this.taxRate,
shippingCost: this.shippingCost,
invoiceNotes: this.invoiceNotes,
logoColor: this.logoColor,
timestamp: new Date().toISOString(),
};
const dataStr = JSON.stringify(data, null, 2);
const dataUri =
"data:application/json;charset=utf-8," +
encodeURIComponent(dataStr);
const clientName = this.cliente.nome || "preventivo";
const exportFileDefaultName = `${clientName}_${new Date().toISOString().split("T")[0]}.json`;
const linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute(
"download",
exportFileDefaultName,
);
linkElement.click();
this.showNotification(
"Dati salvati con successo!",
"success",
);
},
caricaDati() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(
event.target.result,
);
this.venditore =
data.venditore || this.venditore;
this.cliente = data.cliente || this.cliente;
this.items = data.items || [];
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",
);
} catch (error) {
this.showNotification(
"Errore nel caricamento dei dati",
"error",
);
}
};
reader.readAsText(file);
};
input.click();
},
stampa() {
window.print();
},
resetForm() {
if (
confirm(
"Sei sicuro di voler resettare tutti i campi?",
)
) {
this.cliente = {
nome: "",
email: "",
telefono: "",
data: new Date().toISOString().split("T")[0],
};
this.items = [];
this.shippingCost = 0;
this.invoiceNotes = "";
this.addItem();
this.showNotification("Form resettato!", "info");
}
},
};
}
</script>
</body>
</html>