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

38
.dockerignore Normal file
View File

@@ -0,0 +1,38 @@
# Git
.git
.gitignore
.gitea
# Helm
helm-chart/
*.md
# CI/CD
.gitlab-ci.yml
.github/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Backup files
*.bak
*.tmp
*.orig
# Documentation
README.md
LICENSE
docs/
# Node modules (if any)
node_modules/
package-lock.json
yarn.lock

View File

@@ -1,7 +1,70 @@
# Multi-stage build for optimized nginx image
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
# Remove default nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Copy static files
COPY index.html /usr/share/nginx/html/
COPY project-mode.html /usr/share/nginx/html/
COPY shop-mode.html /usr/share/nginx/html/
# Create a custom nginx configuration for better caching and security
RUN cat > /etc/nginx/conf.d/default.conf <<'EOF'
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Main location
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
EOF
# Run nginx as non-root user
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx
# Expose port 80
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

345
README.md
View File

@@ -1,77 +1,308 @@
# Calcolatore Prezzi Software
Un'applicazione web per calcolare i prezzi di progetti software in base a diversi parametri e metodologie di stima.
Sistema professionale per la creazione di preventivi software con due modalità operative:
- **Menu Principale** (`index.html`): Pagina di selezione della modalità con interfaccia intuitiva
- **Modalità Progetto** (`project-mode.html`): Calcolo basato su ore, persone, milestone e regime fiscale italiano
- **Modalità Negozio** (`shop-mode.html`): Calcolo basato su articoli, quantità e listino prezzi (come un e-commerce)
## Caratteristiche
- Interfaccia utente moderna e responsive
- Calcolo automatico dei prezzi basato su parametri configurabili
- Supporto per diverse metodologie di stima
- Design ottimizzato per il mercato italiano
### Modalità Standard
## Installazione e Utilizzo
- ✅ Gestione team con tariffe personalizzate per membro
- ✅ Calcolo milestone con assegnazione task a membri specifici
- ✅ Supporto per diversi regimi fiscali italiani (Forfettario, Ordinario, SRL, SRLS, ecc.)
- ✅ Calcolo automatico di IRPEF, IRES, IRAP, INPS
- ✅ Generazione PDF per cliente e documento interno
- ✅ Salvataggio e caricamento preventivi
- ✅ Interfaccia responsive con Alpine.js e Tailwind CSS
### Utilizzo con Docker
### Modalità Negozio
1. **Costruire l'immagine Docker:**
```bash
docker build -t git.commandware.com/dnviti/calcolatore_prezzi_software .
```
2. **Eseguire il container:**
```bash
docker run -p 8080:80 git.commandware.com/dnviti/calcolatore_prezzi_software
```
3. **Oppure utilizzare l'immagine dal registry:**
```bash
docker pull git.commandware.com/dnviti/calcolatore_prezzi_software:main
docker run -p 8080:80 git.commandware.com/dnviti/calcolatore_prezzi_software:main
```
3. **Accedere all'applicazione:**
Aprire il browser e navigare a `http://localhost:8080`
### Sviluppo Locale
Per sviluppo locale, aprire semplicemente il file `index.html` in un browser web.
## Deployment
Il progetto include una pipeline CI/CD per Gitea che automaticamente:
- Costruisce l'immagine Docker
- Pubblica sul registry Gitea (git.commandware.com)
- Si attiva su push al branch `main` o su pull request
### Configurazione Secrets
Per utilizzare la pipeline, configurare i seguenti secrets nel repository Gitea:
- `GITEA_USERNAME`: Username Gitea
- `GITEA_TOKEN`: Token di accesso Gitea con permessi di scrittura al registry
- ✅ Gestione catalogo articoli con codici SKU
- ✅ Calcolo prezzi unitari e quantità
- ✅ Sconti per articolo
- ✅ Gestione spese di spedizione
- ✅ Calcolo IVA configurabile
- ✅ Generazione PDF preventivo
- ✅ Interfaccia intuitiva per vendita prodotti/servizi
## Struttura del Progetto
```
.
├── index.html # Applicazione web principale
├── Dockerfile # Configurazione Docker
├── .gitea/
│ └── workflows/
│ └── build.yml # Pipeline CI/CD
└── README.md # Documentazione
├── index.html # Menu principale per selezione modalità
├── project-mode.html # Modalità progetto (ore/persone/milestone)
├── shop-mode.html # Modalità negozio (articoli/quantità)
├── Dockerfile # Build immagine Docker
├── deploy.sh # Script deploy automatico
├── .dockerignore # Ignore file per Docker
└── helm-chart/ # Helm chart per Kubernetes
├── Chart.yaml # Definizione chart
├── values.yaml # Valori di configurazione
├── README.md # Documentazione Helm
├── .helmignore # Ignore file per Helm
└── templates/ # Template Kubernetes
├── _helpers.tpl # Helper functions
├── configmap.yaml # ConfigMap per HTML
└── NOTES.txt # Note post-install
```
## Quick Start
### Utilizzo Locale
1. **Apri direttamente nel browser:**
```bash
# Modalità standard
firefox index.html
# Modalità negozio
firefox shop-mode.html
```
2. **Oppure con un server web locale:**
```bash
# Python 3
python3 -m http.server 8000
# PHP
php -S localhost:8000
# Node.js (con http-server)
npx http-server -p 8000
```
Poi visita: `http://localhost:8000`
### Deploy con Docker
1. **Build dell'immagine:**
```bash
docker build -t calcolatore-prezzi:latest .
```
2. **Run del container:**
```bash
docker run -d -p 8080:80 --name calcolatore calcolatore-prezzi:latest
```
3. **Accesso:**
```
http://localhost:8080/ # Menu principale
http://localhost:8080/project-mode.html # Modalità progetto
http://localhost:8080/shop-mode.html # Modalità negozio
```
### Deploy su Kubernetes con Helm
#### Prerequisiti
- Kubernetes cluster (1.19+)
- Helm 3.0+
- kubectl configurato
#### Deploy Rapido
1. **Aggiorna le dipendenze Helm:**
```bash
cd helm-chart
helm dependency update
cd ..
```
2. **Deploy con lo script automatico:**
```bash
# Deploy base
./deploy.sh
# Deploy in namespace specifico
./deploy.sh -n production
# Deploy con valori personalizzati
./deploy.sh -f values-prod.yaml -n production -t v1.0.0
# Upgrade deployment esistente
./deploy.sh -u -n production -t v1.0.1
```
3. **Deploy manuale con Helm:**
```bash
helm install calcolatore-prezzi ./helm-chart \
--namespace production \
--create-namespace \
--set-file configMaps.html-content.data.index\.html=./index.html \
--set-file configMaps.html-content.data.project-mode\.html=./project-mode.html \
--set-file configMaps.html-content.data.shop-mode\.html=./shop-mode.html
```
#### Configurazione Ingress
Modifica `helm-chart/values.yaml`:
```yaml
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: calcolatore.tuodominio.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: calcolatore-tls
hosts:
- calcolatore.tuodominio.com
```
#### Verifica Deploy
```bash
# Stato dei pod
kubectl get pods -n production
# Logs
kubectl logs -n production -l app.kubernetes.io/name=calcolatore-prezzi -f
# Port-forward per test locale
kubectl port-forward -n production svc/calcolatore-prezzi 8080:80
```
## Helm Chart
Il chart utilizza [base-helm](https://git.commandware.com/GitOps/base-helm.git) come dipendenza comune per standardizzare le risorse Kubernetes.
### Parametri Principali
| Parametro | Descrizione | Default |
| ------------------------- | ------------------- | ----------- |
| `replicaCount` | Numero di repliche | `2` |
| `image.repository` | Repository immagine | `nginx` |
| `image.tag` | Tag immagine | `alpine` |
| `service.type` | Tipo service | `ClusterIP` |
| `service.port` | Porta service | `80` |
| `ingress.enabled` | Abilita ingress | `true` |
| `resources.limits.cpu` | Limite CPU | `200m` |
| `resources.limits.memory` | Limite memoria | `256Mi` |
| `autoscaling.enabled` | Abilita HPA | `false` |
Vedi `helm-chart/README.md` per la documentazione completa.
## CI/CD Examples
### GitHub Actions
```yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t registry.example.com/calcolatore:${{ github.sha }} .
- name: Push image
run: docker push registry.example.com/calcolatore:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
helm dependency update ./helm-chart
./deploy.sh -u -n production -t ${{ github.sha }}
```
### GitLab CI
```yaml
stages:
- build
- deploy
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
deploy:
stage: deploy
script:
- helm dependency update ./helm-chart
- ./deploy.sh -u -n production -t $CI_COMMIT_SHA
only:
- main
```
## Tecnologie Utilizzate
- HTML5
- CSS3 (Tailwind CSS)
- JavaScript
- Nginx (per serving statico)
- Docker
- Gitea Actions
- **Frontend:** HTML5, CSS3, JavaScript (Vanilla + Alpine.js)
- **Styling:** Tailwind CSS (via CDN)
- **PDF Generation:** jsPDF
- **Icons:** Font Awesome
- **Containerization:** Docker
- **Orchestration:** Kubernetes + Helm 3
- **Web Server:** Nginx Alpine
## Funzionalità Aggiuntive
### Modalità Standard
- Calcolo dettagliato per regime fiscale italiano
- Supporto INPS, rivalsa, ritenuta d'acconto
- Gestione team con tariffe differenziate
- Milestone con task assegnabili a membri specifici
- Due PDF separati: cliente (pubblico) e interno (riservato)
### Modalità Negozio
- Gestione inventario articoli
- Codici SKU personalizzabili
- Sconti per singolo articolo
- Calcolo spese di spedizione
- IVA configurabile (0%, 4%, 10%, 22%)
## Browser Supportati
- Chrome/Chromium 90+
- Firefox 88+
- Safari 14+
- Edge 90+
## Licenza
[Inserire informazioni sulla licenza]
[Inserisci qui la tua licenza]
## Autore
[Il tuo nome]
## Supporto
Per problemi o domande, apri una issue nel repository.
## Contributing
Le pull request sono benvenute. Per modifiche importanti, apri prima una issue per discutere cosa vorresti cambiare.
---
**Note:**
- Questo progetto è pensato per il mercato italiano e include calcoli fiscali specifici
- I calcoli fiscali sono indicativi e dovresti sempre consultare un commercialista
- Il chart Helm usa base-helm come standard per deployment Kubernetes

234
deploy.sh Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/bash
# Deploy script for Calcolatore Prezzi Software
# This script helps deploy the application to Kubernetes using Helm
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
CHART_PATH="./helm-chart"
RELEASE_NAME="calcolatore-prezzi"
NAMESPACE="default"
VALUES_FILE=""
IMAGE_TAG="latest"
# Functions
print_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Deploy Calcolatore Prezzi Software to Kubernetes using Helm
OPTIONS:
-h, --help Show this help message
-n, --namespace NAME Kubernetes namespace (default: default)
-r, --release NAME Helm release name (default: calcolatore-prezzi)
-f, --values FILE Values file for Helm
-t, --tag TAG Docker image tag (default: latest)
-u, --upgrade Upgrade existing release
-d, --dry-run Perform a dry run
--uninstall Uninstall the release
--build Build Docker image before deploy
EXAMPLES:
# Basic deployment
$0
# Deploy with custom values
$0 -f values-production.yaml -n production
# Upgrade existing deployment
$0 -u -t v1.2.3 -n production
# Dry run
$0 -d -f values-production.yaml
# Build and deploy
$0 --build -t v1.0.0
# Uninstall
$0 --uninstall -n production
EOF
}
check_requirements() {
print_info "Checking requirements..."
if ! command -v helm &> /dev/null; then
print_error "Helm is not installed. Please install Helm 3.0+"
exit 1
fi
if ! command -v kubectl &> /dev/null; then
print_error "kubectl is not installed. Please install kubectl"
exit 1
fi
print_info "All requirements satisfied"
}
update_dependencies() {
print_info "Updating Helm dependencies..."
cd "$CHART_PATH"
helm dependency update
cd - > /dev/null
print_info "Dependencies updated"
}
build_image() {
print_info "Building Docker image..."
docker build -t "calcolatore-prezzi:${IMAGE_TAG}" .
print_info "Docker image built successfully: calcolatore-prezzi:${IMAGE_TAG}"
}
deploy() {
local action="install"
local dry_run=""
if [ "$UPGRADE" = true ]; then
action="upgrade"
fi
if [ "$DRY_RUN" = true ]; then
dry_run="--dry-run --debug"
fi
print_info "Preparing to ${action} release '${RELEASE_NAME}' in namespace '${NAMESPACE}'..."
# Build helm command
local helm_cmd="helm ${action} ${RELEASE_NAME} ${CHART_PATH}"
helm_cmd="${helm_cmd} --namespace ${NAMESPACE}"
helm_cmd="${helm_cmd} --create-namespace"
if [ -n "$VALUES_FILE" ]; then
helm_cmd="${helm_cmd} -f ${VALUES_FILE}"
fi
helm_cmd="${helm_cmd} --set image.tag=${IMAGE_TAG}"
# Inject HTML files
helm_cmd="${helm_cmd} --set-file configMaps.html-content.data.index\\.html=./index.html"
helm_cmd="${helm_cmd} --set-file configMaps.html-content.data.project-mode\\.html=./project-mode.html"
helm_cmd="${helm_cmd} --set-file configMaps.html-content.data.shop-mode\\.html=./shop-mode.html"
if [ -n "$dry_run" ]; then
helm_cmd="${helm_cmd} ${dry_run}"
fi
print_info "Executing: ${helm_cmd}"
eval "$helm_cmd"
if [ "$DRY_RUN" != true ]; then
print_info "Deployment successful!"
print_info "Checking deployment status..."
kubectl rollout status deployment/${RELEASE_NAME} -n ${NAMESPACE} --timeout=5m || true
fi
}
uninstall() {
print_warn "Uninstalling release '${RELEASE_NAME}' from namespace '${NAMESPACE}'..."
helm uninstall ${RELEASE_NAME} -n ${NAMESPACE}
print_info "Release uninstalled successfully"
}
# Parse arguments
UPGRADE=false
DRY_RUN=false
UNINSTALL=false
BUILD=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-r|--release)
RELEASE_NAME="$2"
shift 2
;;
-f|--values)
VALUES_FILE="$2"
shift 2
;;
-t|--tag)
IMAGE_TAG="$2"
shift 2
;;
-u|--upgrade)
UPGRADE=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
--uninstall)
UNINSTALL=true
shift
;;
--build)
BUILD=true
shift
;;
*)
print_error "Unknown option: $1"
usage
exit 1
;;
esac
done
# Main execution
print_info "===== Calcolatore Prezzi Software - Deploy Script ====="
check_requirements
if [ "$UNINSTALL" = true ]; then
uninstall
exit 0
fi
if [ "$BUILD" = true ]; then
build_image
fi
update_dependencies
deploy
print_info "===== Deployment Complete ====="
if [ "$DRY_RUN" != true ]; then
print_info ""
print_info "To check the status:"
print_info " kubectl get pods -n ${NAMESPACE} -l app.kubernetes.io/name=${RELEASE_NAME}"
print_info ""
print_info "To view logs:"
print_info " kubectl logs -n ${NAMESPACE} -l app.kubernetes.io/name=${RELEASE_NAME} -f"
print_info ""
print_info "To access the application:"
print_info " kubectl port-forward -n ${NAMESPACE} svc/${RELEASE_NAME} 8080:80"
print_info " Then visit: http://localhost:8080"
fi

26
helm-chart/.helmignore Normal file
View File

@@ -0,0 +1,26 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
# Custom
README.md
.helmignore

26
helm-chart/Chart.yaml Normal file
View File

@@ -0,0 +1,26 @@
apiVersion: v2
name: calcolatore-prezzi-software
description: A Helm chart for Calcolatore Prezzi Software application
type: application
version: 1.0.0
appVersion: "1.0.0"
# Dependency on base-helm common chart
dependencies:
- name: base-helm
version: "*"
repository: "https://git.commandware.com/GitOps/base-helm.git"
alias: common
maintainers:
- name: Your Name
email: your-email@example.com
keywords:
- calculator
- pricing
- software
- static-website
sources:
- https://github.com/your-repo/calcolatore-prezzi-software

300
helm-chart/README.md Normal file
View File

@@ -0,0 +1,300 @@
# Calcolatore Prezzi Software - Helm Chart
Questo è un Helm chart semplificato per il deploy del Calcolatore Prezzi Software che utilizza il [base-helm](https://git.commandware.com/GitOps/base-helm.git) come chart comune.
## Prerequisiti
- Kubernetes 1.19+
- Helm 3.0+
- Nginx Ingress Controller (opzionale, per l'ingress)
- Cert-manager (opzionale, per i certificati SSL)
## Installazione
### 1. Aggiornare le dipendenze
Prima di installare il chart, è necessario aggiornare le dipendenze per scaricare il base-helm:
```bash
cd helm-chart
helm dependency update
```
### 2. Installazione base
```bash
helm install calcolatore-prezzi ./helm-chart
```
### 3. Installazione con valori personalizzati
```bash
helm install calcolatore-prezzi ./helm-chart -f custom-values.yaml
```
### 4. Installazione con file HTML personalizzati
Per iniettare i tuoi file HTML durante l'installazione:
```bash
helm install calcolatore-prezzi ./helm-chart \
--set-file configMaps.html-content.data.index\.html=./index.html \
--set-file configMaps.html-content.data.shop-mode\.html=./shop-mode.html
```
### 5. Installazione in un namespace specifico
```bash
kubectl create namespace calcolatore
helm install calcolatore-prezzi ./helm-chart -n calcolatore
```
## Configurazione
I seguenti parametri possono essere configurati nel file `values.yaml`:
### Parametri Applicazione
| Parametro | Descrizione | Default |
|-----------|-------------|---------|
| `replicaCount` | Numero di repliche del pod | `2` |
| `image.repository` | Repository dell'immagine Docker | `nginx` |
| `image.tag` | Tag dell'immagine Docker | `alpine` |
| `image.pullPolicy` | Policy di pull dell'immagine | `IfNotPresent` |
### Parametri Service
| Parametro | Descrizione | Default |
|-----------|-------------|---------|
| `service.type` | Tipo di service Kubernetes | `ClusterIP` |
| `service.port` | Porta del service | `80` |
| `service.targetPort` | Porta target del container | `80` |
### Parametri Ingress
| Parametro | Descrizione | Default |
|-----------|-------------|---------|
| `ingress.enabled` | Abilita l'ingress | `true` |
| `ingress.className` | Classe dell'ingress controller | `nginx` |
| `ingress.hosts[0].host` | Hostname per l'ingress | `calcolatore.example.com` |
| `ingress.tls[0].secretName` | Nome del secret TLS | `calcolatore-tls` |
### Parametri Risorse
| Parametro | Descrizione | Default |
|-----------|-------------|---------|
| `resources.limits.cpu` | Limite CPU | `200m` |
| `resources.limits.memory` | Limite memoria | `256Mi` |
| `resources.requests.cpu` | Request CPU | `100m` |
| `resources.requests.memory` | Request memoria | `128Mi` |
### Parametri Autoscaling
| Parametro | Descrizione | Default |
|-----------|-------------|---------|
| `autoscaling.enabled` | Abilita l'HPA | `false` |
| `autoscaling.minReplicas` | Numero minimo di repliche | `2` |
| `autoscaling.maxReplicas` | Numero massimo di repliche | `5` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU per scaling | `80` |
## Esempi di Configurazione
### values-production.yaml
```yaml
replicaCount: 3
image:
repository: your-registry.io/calcolatore-prezzi
tag: "1.0.0"
pullPolicy: Always
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
hosts:
- host: calcolatore.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: calcolatore-prod-tls
hosts:
- calcolatore.yourdomain.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 2
```
### values-development.yaml
```yaml
replicaCount: 1
image:
repository: nginx
tag: "alpine"
pullPolicy: IfNotPresent
ingress:
enabled: true
className: "nginx"
hosts:
- host: calcolatore.dev.local
paths:
- path: /
pathType: Prefix
tls: []
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
autoscaling:
enabled: false
```
## Aggiornamento
Per aggiornare il deployment:
```bash
helm upgrade calcolatore-prezzi ./helm-chart
```
Con file HTML aggiornati:
```bash
helm upgrade calcolatore-prezzi ./helm-chart \
--set-file configMaps.html-content.data.index\.html=./index.html \
--set-file configMaps.html-content.data.shop-mode\.html=./shop-mode.html
```
## Disinstallazione
```bash
helm uninstall calcolatore-prezzi
```
## Testing
Per testare il chart senza installarlo:
```bash
# Dry run
helm install calcolatore-prezzi ./helm-chart --dry-run --debug
# Template rendering
helm template calcolatore-prezzi ./helm-chart
# Lint
helm lint ./helm-chart
```
## Deploy con CI/CD
### GitLab CI Example
```yaml
deploy:
stage: deploy
image: alpine/helm:latest
script:
- helm dependency update ./helm-chart
- helm upgrade --install calcolatore-prezzi ./helm-chart
--namespace production
--create-namespace
--set image.tag=$CI_COMMIT_SHA
--set-file configMaps.html-content.data.index\.html=./index.html
--set-file configMaps.html-content.data.shop-mode\.html=./shop-mode.html
only:
- main
```
### GitHub Actions Example
```yaml
name: Deploy to Kubernetes
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Helm
uses: azure/setup-helm@v1
with:
version: '3.9.0'
- name: Deploy
run: |
helm dependency update ./helm-chart
helm upgrade --install calcolatore-prezzi ./helm-chart \
--namespace production \
--create-namespace \
--set-file configMaps.html-content.data.index\.html=./index.html \
--set-file configMaps.html-content.data.shop-mode\.html=./shop-mode.html
```
## Troubleshooting
### Verificare lo stato del deployment
```bash
kubectl get pods -l app.kubernetes.io/name=calcolatore-prezzi
kubectl describe pod <pod-name>
kubectl logs <pod-name>
```
### Verificare la configurazione
```bash
helm get values calcolatore-prezzi
helm get manifest calcolatore-prezzi
```
### Verificare l'ingress
```bash
kubectl get ingress
kubectl describe ingress calcolatore-prezzi
```
## Note
- Questo chart usa il base-helm come dipendenza per standardizzare le risorse Kubernetes
- I file HTML vengono iniettati tramite ConfigMap
- Per ambienti di produzione, considera l'uso di un registry Docker privato e immagini custom
- Assicurati di configurare correttamente i certificati SSL per la produzione
## Supporto
Per problemi o domande, apri una issue nel repository del progetto.

View File

@@ -0,0 +1,27 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "common.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "common.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "common.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "common.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
2. The Calcolatore Prezzi Software application has been deployed!
- Main menu: /index.html
- Project mode: /project-mode.html
- Shop mode: /shop-mode.html

View File

@@ -0,0 +1,60 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "calcolatore.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "calcolatore.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "calcolatore.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "calcolatore.labels" -}}
helm.sh/chart: {{ include "calcolatore.chart" . }}
{{ include "calcolatore.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "calcolatore.selectorLabels" -}}
app.kubernetes.io/name: {{ include "calcolatore.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "calcolatore.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "calcolatore.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,53 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "calcolatore.fullname" . }}-html-content
labels:
{{- include "calcolatore.labels" . | nindent 4 }}
data:
# Note: In a real deployment, you would use a CI/CD pipeline to inject these files
# or mount them from a separate volume. This is just a simple example.
# You can also use --set-file flag with helm to inject files:
# helm install myapp ./helm-chart \
# --set-file configMaps.html-content.data.index\.html=./index.html \
# --set-file configMaps.html-content.data.project-mode\.html=./project-mode.html \
# --set-file configMaps.html-content.data.shop-mode\.html=./shop-mode.html
index.html: |
<!-- Menu/Landing page - Content will be injected during deployment -->
<!DOCTYPE html>
<html>
<head>
<title>Calcolatore Prezzi - Selezione Modalità</title>
<meta http-equiv="refresh" content="0; url=https://example.com/">
</head>
<body>
<p>Loading...</p>
</body>
</html>
project-mode.html: |
<!-- Project mode - Content will be injected during deployment -->
<!DOCTYPE html>
<html>
<head>
<title>Calcolatore Prezzi - Modalità Progetto</title>
<meta http-equiv="refresh" content="0; url=https://example.com/project-mode.html">
</head>
<body>
<p>Loading...</p>
</body>
</html>
shop-mode.html: |
<!-- Shop mode - Content will be injected during deployment -->
<!DOCTYPE html>
<html>
<head>
<title>Calcolatore Prezzi - Modalità Negozio</title>
<meta http-equiv="refresh" content="0; url=https://example.com/shop-mode.html">
</head>
<body>
<p>Loading...</p>
</body>
</html>

187
helm-chart/values.yaml Normal file
View File

@@ -0,0 +1,187 @@
# Default values for calcolatore-prezzi-software
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Use base-helm common chart for standard configurations
common:
# Name override
nameOverride: ""
fullnameOverride: "calcolatore-prezzi"
# Application configuration
replicaCount: 2
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "alpine"
imagePullSecrets: []
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
runAsNonRoot: true
runAsUser: 1000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
service:
type: ClusterIP
port: 80
targetPort: 80
annotations: {}
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: calcolatore.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: calcolatore-tls
hosts:
- calcolatore.example.com
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- calcolatore-prezzi
topologyKey: kubernetes.io/hostname
# Liveness and Readiness probes
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
# Volume mounts for static files
volumeMounts:
- name: html-content
mountPath: /usr/share/nginx/html
readOnly: true
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /var/run
volumes:
- name: html-content
configMap:
name: calcolatore-html-content
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
# ConfigMap for HTML files
configMaps:
html-content:
data:
# The HTML files will be injected here
# In production, you would use a CI/CD pipeline to update these
index.html: |
<!-- Your index.html content will be here -->
shop-mode.html: |
<!-- Your shop-mode.html content will be here -->
# Environment variables
env: []
# - name: ENVIRONMENT
# value: "production"
# Additional labels
labels: {}
# Pod Disruption Budget
podDisruptionBudget:
enabled: true
minAvailable: 1
# Network Policy
networkPolicy:
enabled: false
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 80
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
# Monitoring
monitoring:
enabled: false
serviceMonitor:
enabled: false
interval: 30s
path: /metrics

1898
index.html

File diff suppressed because it is too large Load Diff

1499
project-mode.html Normal file

File diff suppressed because it is too large Load Diff

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>