- 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
1448 lines
65 KiB
HTML
1448 lines
65 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 imgData = ctx.getImageData(
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height,
|
|
);
|
|
const data = imgData.data;
|
|
|
|
// Collect color palette with counts
|
|
const colorMap = {};
|
|
for (let i = 0; i < data.length; i += 16) {
|
|
// Sample every 4 pixels for better coverage
|
|
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 whites (> 240 in all channels)
|
|
if (r > 240 && g > 240 && b > 240) continue;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Build palette with color scores
|
|
const palette = [];
|
|
for (const [rgb, count] of Object.entries(
|
|
colorMap,
|
|
)) {
|
|
const [r, g, b] = rgb
|
|
.split(",")
|
|
.map(Number);
|
|
|
|
// 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 {
|
|
this.logoColor = "#10b981"; // Fallback emerald
|
|
console.log(
|
|
"No suitable colors found, using fallback",
|
|
);
|
|
}
|
|
} 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 = 0;
|
|
|
|
// 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 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");
|
|
|
|
yPos = 18;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
);
|
|
|
|
doc.setFontSize(14);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.text("PREVENTIVO", xStart, yPos + 23);
|
|
|
|
// 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" });
|
|
|
|
yPos = 65;
|
|
|
|
// ===== 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");
|
|
|
|
// 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");
|
|
|
|
doc.setFontSize(9);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.text("VENDITORE", 57.5, yPos + 5.5, {
|
|
align: "center",
|
|
});
|
|
|
|
// 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,
|
|
);
|
|
|
|
// 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");
|
|
|
|
// 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");
|
|
|
|
doc.setFontSize(9);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.text("CLIENTE", 152.5, yPos + 5.5, {
|
|
align: "center",
|
|
});
|
|
|
|
// 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);
|
|
|
|
yPos += 38;
|
|
|
|
// ===== 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.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" });
|
|
|
|
yPos += 14;
|
|
|
|
// Items rows with alternating background
|
|
let itemIndex = 0;
|
|
this.items.forEach((item) => {
|
|
if (yPos > 255) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
itemIndex = 0;
|
|
}
|
|
|
|
// Alternating row background
|
|
if (itemIndex % 2 === 0) {
|
|
doc.setFillColor(250, 250, 250);
|
|
doc.rect(15, yPos - 2, 180, 10, "F");
|
|
}
|
|
|
|
// Item data
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(60, 60, 60);
|
|
doc.text(item.name.substring(0, 45), 18, yPos + 2);
|
|
|
|
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,
|
|
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>
|