Add multi-mode HTML, Docker, Helm chart, and deploy script
All checks were successful
Build and Deploy / build (push) Successful in 46s

- Add shop-mode.html and project-mode.html for separate calculation
modes - Refactor index.html as a landing page for mode selection - Add
Dockerfile with optimized nginx config and healthcheck - Add
.dockerignore for cleaner Docker builds - Add deploy.sh for
Helm/Kubernetes deployment automation - Add helm-chart/ with
values.yaml, Chart.yaml, templates, and documentation - Update README.md
with full instructions, features, and CI/CD examples
This commit is contained in:
d.viti
2025-10-13 23:25:33 +02:00
parent 68d1c91456
commit 23ec5d5f32
14 changed files with 4021 additions and 1553 deletions

658
shop-mode.html Normal file
View File

@@ -0,0 +1,658 @@
<!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>
<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 class="flex-1 text-right">
<span class="text-xs text-gray-600">Prezzo scontato: </span>
<span class="font-bold text-emerald-600" x-text="'€' + calculateDiscountedTotal(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>
<select x-model.number="taxRate"
class="w-full p-3 border-2 border-emerald-200 rounded-lg focus:border-emerald-500 focus:outline-none">
<option value="0">Esente IVA (0%)</option>
<option value="4">4% (Beni di prima necessità)</option>
<option value="10">10% (Ridotta)</option>
<option value="22">22% (Ordinaria)</option>
</select>
</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: ''
},
cliente: {
nome: '',
email: '',
telefono: '',
data: new Date().toISOString().split('T')[0]
},
items: [],
itemCounter: 0,
taxRate: 22,
shippingCost: 0,
invoiceNotes: '',
init() {
this.addItem();
},
addItem() {
this.itemCounter++;
this.items.push({
id: this.itemCounter,
name: '',
sku: '',
description: '',
price: 0,
quantity: 1,
discount: 0
});
},
removeItem(index) {
this.items.splice(index, 1);
},
calculateDiscountedTotal(item) {
const subtotal = item.price * item.quantity;
const discount = subtotal * (item.discount / 100);
return subtotal - discount;
},
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() {
return this.calculateTaxableAmount() * (this.taxRate / 100);
},
calculateTotal() {
return this.calculateTaxableAmount() + this.calculateTax();
},
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;
// Header
doc.setFontSize(20);
doc.setTextColor(16, 185, 129);
doc.text(this.venditore.nome || 'Venditore', 105, yPos, { align: 'center' });
yPos += 10;
doc.setFontSize(14);
doc.setTextColor(100);
doc.text('PREVENTIVO', 105, yPos, { align: 'center' });
yPos += 15;
// 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
doc.setFontSize(10);
doc.setFillColor(16, 185, 129);
doc.rect(20, yPos, 170, 7, 'F');
doc.setTextColor(255);
doc.text('Articolo', 25, yPos + 5);
doc.text('Q.tà', 120, yPos + 5);
doc.text('Prezzo', 140, yPos + 5);
doc.text('Sconto', 160, yPos + 5);
doc.text('Totale', 175, yPos + 5);
yPos += 10;
doc.setTextColor(0);
this.items.forEach(item => {
if (yPos > 270) {
doc.addPage();
yPos = 20;
}
doc.text(item.name.substring(0, 40), 25, yPos);
doc.text(String(item.quantity), 125, yPos);
doc.text(`${item.price.toFixed(2)}`, 140, yPos);
doc.text(`${item.discount}%`, 163, yPos);
doc.text(`${this.calculateDiscountedTotal(item).toFixed(2)}`, 175, yPos);
yPos += 7;
});
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(12);
doc.setFont(undefined, 'bold');
doc.text('TOTALE:', 120, yPos);
doc.text(this.formatCurrency(this.calculateTotal()), 175, 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,
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.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>