<!DOCTYPE html> <html lang="it"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Prompt v0.app — i.Blue Configuratore</title> <style> *{box-sizing:border-box;margin:0;padding:0;} body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;} header{background:linear-gradient(135deg,#0b4d91,#1272c8);padding:24px 20px;text-align:center;} header h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-0.5px;} header p{font-size:12px;color:rgba(255,255,255,.7);margin-top:6px;} .wrap{max-width:900px;margin:0 auto;padding:24px 16px;} .step-badge{display:inline-flex;align-items:center;gap:8px;background:#1e3a5f;border:1px solid #2563eb;border-radius:20px;padding:6px 14px;font-size:12px;font-weight:700;color:#93c5fd;margin-bottom:12px;} .step-badge .num{background:#2563eb;color:#fff;border-radius:50%;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:900;} .card{background:#1e293b;border:1px solid #334155;border-radius:14px;padding:20px;margin-bottom:20px;} .card h2{font-size:16px;font-weight:800;color:#f1f5f9;margin-bottom:6px;} .card .desc{font-size:13px;color:#94a3b8;margin-bottom:14px;line-height:1.6;} .card .warn{font-size:12px;color:#fbbf24;background:#2d2000;border:1px solid #92400e;border-radius:8px;padding:8px 12px;margin-bottom:14px;} .prompt-box{background:#0f172a;border:1px solid #475569;border-radius:10px;overflow:hidden;} .prompt-header{display:flex;justify-content:space-between;align-items:center;padding:8px 14px;background:#1e293b;border-bottom:1px solid #334155;} .prompt-label{font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:1px;} .copy-btn{background:#2563eb;color:#fff;border:none;border-radius:7px;padding:5px 14px;font-size:12px;font-weight:700;cursor:pointer;transition:background .15s;} .copy-btn:hover{background:#1d4ed8;} .copy-btn.copied{background:#16a34a;} .prompt-text{padding:14px;font-family:'Courier New',monospace;font-size:12px;line-height:1.6;color:#e2e8f0;white-space:pre-wrap;word-break:break-word;max-height:300px;overflow-y:auto;} .prompt-text.tall{max-height:500px;} .prompt-text.full{max-height:none;} .divider{border:none;border-top:1px solid #334155;margin:10px 0;} .note{font-size:12px;color:#64748b;margin-top:10px;line-height:1.6;} .tag{display:inline-block;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:700;margin-right:4px;} .tag-blue{background:#1e3a5f;color:#93c5fd;} .tag-green{background:#14532d;color:#86efac;} .tag-yellow{background:#422006;color:#fcd34d;} .tag-red{background:#450a0a;color:#fca5a5;} .checklist{list-style:none;display:flex;flex-direction:column;gap:6px;margin-top:10px;} .checklist li{font-size:12px;color:#94a3b8;padding-left:20px;position:relative;} .checklist li::before{content:"✓";position:absolute;left:0;color:#22c55e;font-weight:700;} .checklist li.warn::before{content:"⚠";color:#f59e0b;} </style> </head> <body> <header> <h1>🏊 Prompt v0.app — i.Blue Configuratore Professionale</h1> <p>Copia i prompt nell'ordine indicato su v0.app per ottenere la versione potenziata con Next.js + Tailwind + shadcn/ui</p> </header> <div class="wrap"> <!-- ISTRUZIONI RAPIDE --> <div class="card"> <h2>📋 Istruzioni rapide</h2> <div class="desc">Segui questi passi nell'ordine esatto. Ogni prompt va incollato come messaggio su <strong>v0.app</strong> in una nuova chat.</div> <ul class="checklist"> <li>Vai su <strong>v0.app</strong> → accedi con Vercel o GitHub account</li> <li>Crea una <strong>nuova chat</strong> (tasto + in alto a sinistra)</li> <li>Incolla <strong>Prompt 1</strong> → invia → aspetta la risposta</li> <li>Incolla <strong>Prompt 2</strong> (il codice) nello <strong>stesso thread</strong> → invia</li> <li>Aspetta il rendering completo (30-60 sec) → verifica l'anteprima</li> <li>Usa i <strong>Prompt di upgrade</strong> nel STESSO thread per aggiungere funzioni</li> <li class="warn">NON iniziare una nuova chat per gli upgrade — v0 deve ricordare il contesto</li> </ul> </div> <!-- PROMPT 1 --> <div class="step-badge"><span class="num">1</span> PRIMO MESSAGGIO — Contesto e istruzioni</div> <div class="card"> <h2>Prompt 1 — Invia questo per primo</h2> <div class="desc">Spiega a v0 cosa deve fare <em>prima</em> di vedere il codice. Questo prepara il modello a non toccare la logica.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Copia e incolla su v0.app</span> <button class="copy-btn" onclick="copyText(this,'p1')">Copia</button> </div> <div class="prompt-text" id="p1">Devo convertire un configuratore/preventivatore professionale per piscine i.Blue già completamente funzionante. È scritto in React con CSS inline e stili custom. Voglio che tu lo converta nella stack nativa di v0: Next.js 14 App Router + Tailwind CSS + shadcn/ui. REGOLE ASSOLUTE — non derogabili: 1. NON modificare NESSUN valore numerico (prezzi, calcoli, percentuali) 2. NON modificare la struttura dati STR, RIV, BORDI, CIRC, FILTR, LUCI, SCALINE, OPT 3. NON modificare il flusso degli 8 step (Cliente → Modello → Struttura → Rivestimento → Idraulica → Filtrazione → Accessori → Preventivo) 4. NON modificare la funzione generaPDF() — deve rimanere identica 5. NON modificare le funzioni di calcolo p(), fmt(), uid() 6. CONVERTI solo: CSS inline → classi Tailwind, div custom → componenti shadcn/ui COSA MIGLIORARE con shadcn/ui: - Usa <Button> per tutti i bottoni - Usa <Card> per le card - Usa <Input> per tutti gli input - Usa <Badge> per i tag colorati - Usa <Sheet> per il pannello archivio (bottom sheet mobile) - Usa <ScrollArea> per le liste scrollabili - Usa <Separator> per i divisori - Usa cn() di class-variance-authority per le classi condizionali - Mantieni colore primario esatto: #0b4d91 - Mantieni il design dark-blue premium Nel prossimo messaggio ti invio il codice completo da convertire.</div> </div> </div> <!-- PROMPT 2 --> <div class="step-badge"><span class="num">2</span> SECONDO MESSAGGIO — Il codice completo</div> <div class="card"> <h2>Prompt 2 — Il codice (incolla subito dopo il Prompt 1)</h2> <div class="desc">Questo è il codice completo del configuratore. Incollalo intero nel secondo messaggio dello stesso thread.</div> <div class="warn">⚠️ Il codice è lungo (~1200 righe). Se v0 tronca, incolla in due parti: prima le righe 1-600, poi un messaggio "continua" con le righe 601-fine.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Codice completo — copia tutto</span> <button class="copy-btn" onclick="copyText(this,'p2')">Copia</button> </div> <div class="prompt-text full" id="p2">Ecco il codice React completo da convertire. Segui tutte le istruzioni del messaggio precedente: ```jsx INSERISCI_QUI_IL_CONTENUTO_COMPLETO_DI_iblue-configurator.jsx ``` Converti questo codice in Next.js 14 + Tailwind + shadcn/ui rispettando tutte le regole. Il file risultante deve essere un singolo componente "use client" pronto per Next.js App Router.</div> </div> <div class="note">💡 Per ottenere il contenuto del file: apri iblue-configurator.jsx, seleziona tutto (Ctrl+A), copia (Ctrl+C), incolla al posto di INSERISCI_QUI_IL_CONTENUTO_COMPLETO_DI_iblue-configurator.jsx</div> </div> <!-- VERIFICA --> <div class="step-badge"><span class="num">3</span> VERIFICA obbligatoria prima degli upgrade</div> <div class="card"> <h2>Prompt 3 — Verifica prezzi e funzioni (invia dopo la conversione)</h2> <div class="desc">Prima di fare qualsiasi upgrade, verifica che la conversione non abbia alterato nulla.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Prompt verifica</span> <button class="copy-btn" onclick="copyText(this,'p3')">Copia</button> </div> <div class="prompt-text" id="p3">Verifica che la conversione sia corretta controllando questi valori esatti: 1. Struttura i.Blue Ambience 4×8 m Skimmer H 1200 mm deve mostrare: 11.287,50 € 2. Struttura i.Blue Inox 5×10 m Skimmer H 1400 mm deve mostrare: 23.829,75 € 3. Struttura Super HD Zincata Sfioro 6×12 m H 1350 mm deve mostrare: 33.078,15 € 4. Rivestimento Elbtal Gloobe 6×12 m Skimmer fondo piatto deve mostrare: 4.968,60 € 5. GloobeLine 32 m³/h pozzetto VTR Pool's deve mostrare: 8.264,55 € 6. Luce RGB 4 fari deve mostrare: 2.999,85 € 7. Lo sconto al 40% su un totale di 20.000 € deve dare netto: 12.000,00 € Se uno di questi valori non corrisponde, correggi SOLO i valori errati senza toccare nulla d'altro.</div> </div> </div> <hr style="border:none;border-top:2px solid #334155;margin:28px 0;"> <div style="text-align:center;margin-bottom:20px;"> <div style="font-size:13px;color:#64748b;font-weight:700;text-transform:uppercase;letter-spacing:1px;">— UPGRADE OPZIONALI — usa nello stesso thread dopo la verifica —</div> </div> <!-- UPGRADE A --> <div class="step-badge"><span class="num">A</span> UPGRADE — Persistenza localStorage</div> <div class="card"> <h2>Upgrade A — Salvataggio automatico (non perde dati al refresh)</h2> <div class="desc">Aggiunge il salvataggio automatico dello stato in localStorage. Il preventivo sopravvive al refresh della pagina.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Upgrade A</span> <button class="copy-btn" onclick="copyText(this,'pa')">Copia</button> </div> <div class="prompt-text" id="pa">Aggiungi localStorage persistence al configuratore. Regole: - NON toccare nessun prezzo, dato tecnico o flusso step - NON modificare generaPDF() Da aggiungere: 1. useEffect che salva l'intero stato q in localStorage ogni volta che cambia (key: "iblue_current") 2. useEffect al mount che ripristina lo stato salvato da localStorage se esiste 3. L'array saved (archivio preventivi) deve essere salvato in localStorage (key: "iblue_archive") e ripristinato al mount 4. Un Banner sottile sopra gli step, visibile solo se c'è un preventivo ripristinato: "Preventivo ripristinato · [numero preventivo]" con un link-bottone "✕ Nuovo preventivo" che resetta lo stato al valore iniziale 5. Il banner deve sparire automaticamente dopo 5 secondi o quando l'utente clicca "✕ Nuovo preventivo"</div> </div> </div> <!-- UPGRADE B --> <div class="step-badge"><span class="num">B</span> UPGRADE — Export Excel</div> <div class="card"> <h2>Upgrade B — Esporta in Excel (.xlsx)</h2> <div class="desc">Aggiunge un bottone che scarica il preventivo come file Excel professionale con due fogli.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Upgrade B</span> <button class="copy-btn" onclick="copyText(this,'pb')">Copia</button> </div> <div class="prompt-text" id="pb">Aggiungi export Excel allo step 7 (Preventivo). Regole: - NON toccare nessun prezzo, dato tecnico o flusso step - NON modificare generaPDF() - Usa la libreria xlsx (SheetJS) importata da CDN o npm Da aggiungere: 1. Funzione generaExcel(q, lines, lordo, netto, sc, mg, model) che crea un file .xlsx 2. Il file deve avere 2 fogli: Foglio "Preventivo": - Riga 1: "i.Blue Piscine – Preventivo [numero]" (merge celle A1:F1, sfondo #0b4d91, testo bianco, bold) - Riga 2 vuota - Righe 3-6: dati cliente (etichetta | valore) su 2 colonne - Riga 7 vuota - Riga 8: intestazioni tabella materiali (#, Codice, Descrizione, Q., P.Unit., Totale) sfondo grigio - Righe 9+: una riga per ogni voce del preventivo, righe alternate bianco/#f8fafc - Ultime 4 righe: Totale Lordo, Sconto %, Margine €, TOTALE NETTO (bold, sfondo #e8f0fb) Foglio "Tecnico": - Dati tecnici piscina (modello, dimensioni, struttura, circolazione ecc.) - Se presente la struttura, includi tutte le righe TECH con etichetta e valore 3. Bottone "📊 Excel" accanto al bottone "📄 PDF" nello step 7 4. Il file si chiama: Preventivo-[numero]-iBlue.xlsx</div> </div> </div> <!-- UPGRADE C --> <div class="step-badge"><span class="num">C</span> UPGRADE — Link di condivisione</div> <div class="card"> <h2>Upgrade C — Condividi preventivo via link</h2> <div class="desc">Genera un URL che apre il preventivo precompilato. Zero backend, tutto client-side.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Upgrade C</span> <button class="copy-btn" onclick="copyText(this,'pc')">Copia</button> </div> <div class="prompt-text" id="pc">Aggiungi condivisione via link al configuratore. Regole: - NON toccare nessun prezzo, dato tecnico o flusso step - NON modificare generaPDF() Da aggiungere: 1. Funzione condividi() che: a. Prende lo stato corrente q (esclude le props _l, _lor, _net, _sc usate solo internamente) b. JSON.stringify → btoa (base64) → encodeURIComponent c. Costruisce URL: window.location.origin + window.location.pathname + "?p=" + encoded d. Copia l'URL negli appunti con navigator.clipboard.writeText() e. Mostra notifica "🔗 Link copiato!" per 3 secondi 2. Al mount dell'app, controlla se esiste ?p= nell'URL: a. Se sì: decodeURIComponent → atob → JSON.parse → setQ(parsed) b. Mostra notifica "📥 Preventivo ricevuto" c. Rimuovi il parametro dall'URL con history.replaceState senza ricaricare 3. Bottone "🔗 Condividi" accanto a "💾 Salva" nell'header in alto 4. Lo stesso bottone nello step 7 (Preventivo) con label "🔗 Copia link preventivo"</div> </div> </div> <!-- UPGRADE D --> <div class="step-badge"><span class="num">D</span> UPGRADE — Rubrica clienti</div> <div class="card"> <h2>Upgrade D — Rubrica clienti con storico</h2> <div class="desc">Salva i clienti in rubrica, precompila i campi nello step Cliente, mostra storico preventivi per cliente.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Upgrade D</span> <button class="copy-btn" onclick="copyText(this,'pd')">Copia</button> </div> <div class="prompt-text" id="pd">Aggiungi rubrica clienti al configuratore. Regole: - NON toccare nessun prezzo, dato tecnico o flusso step - NON modificare generaPDF() Da aggiungere: 1. Stato rubrica: array di oggetti {id, nome, azienda, tel, email} salvato in localStorage (key: "iblue_rubrica") 2. Nello step 0 (Cliente), sopra il form, aggiungi: - Un componente shadcn/ui Select con label "Seleziona cliente salvato" che mostra tutti i clienti salvati - Selezione precompila automaticamente i campi cliente, azienda, tel, email - Un bottone "💾 Salva cliente" che aggiunge/aggiorna il cliente corrente nella rubrica (match su nome+azienda) 3. Nell'archivio preventivi (bottom sheet), per ogni preventivo mostra il nome cliente in modo cliccabile 4. Un bottone "👥 Rubrica" nell'header accanto agli altri che apre un secondo bottom sheet con: - Lista di tutti i clienti salvati - Per ogni cliente: nome, azienda, numero preventivi associati (contati dall'archivio saved) - Bottone "Nuovo preventivo" per quel cliente che compila i campi e va allo step 0 - Bottone "Elimina" per rimuovere il cliente dalla rubrica</div> </div> </div> <!-- UPGRADE E --> <div class="step-badge"><span class="num">E</span> UPGRADE — Anteprima PDF live</div> <div class="card"> <h2>Upgrade E — Pannello anteprima PDF in tempo reale</h2> <div class="desc">Su desktop, mostra a destra un pannello con l'anteprima del documento PDF aggiornata in live mentre configuri.</div> <div class="prompt-box"> <div class="prompt-header"> <span class="prompt-label">Upgrade E</span> <button class="copy-btn" onclick="copyText(this,'pe')">Copia</button> </div> <div class="prompt-text" id="pe">Aggiungi anteprima live del preventivo. Regole: - NON toccare nessun prezzo, dato tecnico o flusso step - NON modificare generaPDF() Da aggiungere: 1. Su schermi ≥1280px: layout a due colonne. Colonna sinistra (flex-1): il configuratore esistente. Colonna destra (w-80 sticky top-4): pannello anteprima 2. Il pannello anteprima mostra in miniatura: - Intestazione i.Blue con numero preventivo e data - Dati cliente (se compilati) - Elenco materiali selezionati finora (massimo 8 righe visibili, poi "...e altri N") - Totale lordo, sconto, totale netto sempre visibili - Un indicatore di completamento: "Step [N]/8 completato" con una progress bar Tailwind 3. Il pannello si aggiorna in tempo reale ad ogni selezione (è derivato dallo stato q e lines, nessun nuovo stato) 4. Quando lines è vuoto, mostra un placeholder: "🏊 Configura la tua piscina — l'anteprima apparirà qui" 5. Su mobile (<1280px): il pannello non appare (hidden), tutto rimane come prima</div> </div> </div> <!-- DEPLOY --> <div class="card" style="border-color:#22c55e;"> <h2 style="color:#86efac;">🚀 Deploy finale su Vercel</h2> <div class="desc">Dopo aver verificato tutto nel preview di v0, clicca <strong>"Deploy"</strong> in alto a destra. Il configuratore sarà online su un URL tipo <strong>iblue-configuratore.vercel.app</strong> in circa 60 secondi. Zero configurazione richiesta.</div> <ul class="checklist"> <li>Verifica anteprima mobile (tasto responsive in v0)</li> <li>Testa il flusso completo fino allo step 7</li> <li>Genera un PDF di prova e controlla i prezzi</li> <li>Clicca Deploy → scegli nome progetto → conferma</li> <li>Condividi l'URL con i tuoi clienti e agenti</li> </ul> </div> </div> <script> function copyText(btn, id) { const text = document.getElementById(id).innerText; navigator.clipboard.writeText(text).then(() => { btn.textContent = "✓ Copiato!"; btn.classList.add("copied"); setTimeout(() => { btn.textContent = "Copia"; btn.classList.remove("copied"); }, 2500); }); } </script> </body> </html>
I'll build the "i.Blue Configurator Pro" scaffolding with all the infrastructure ready. Let me set up the project structure, PWA configuration, and utilities first.
Now let me install the PDF library and set up the project structure:
Now let me create the complete project structure with all the scaffolding:
The scaffolding for i.Blue Configurator Pro is now complete. Here's the folder structure I've created:
/vercel/share/v0-project/
├── app/
│ ├── globals.css (Dark blue premium theme)
│ ├── layout.tsx (PWA meta tags, Italian locale)
│ └── page.tsx (Main entry point)
├── components/
│ ├── app-header.tsx (Header with save/share/archive buttons)
│ ├── archive-sheet.tsx (Bottom sheet for saved quotes)
│ ├── contacts-sheet.tsx (Bottom sheet for client directory)
│ ├── configurator.tsx (Main component - PASTE YOUR CODE HERE)
│ ├── restored-banner.tsx (Banner for restored quotes)
│ └── step-indicator.tsx (8-step progress indicator)
├── data/
│ └── pricing.ts (Placeholder for STR, RIV, BORDI, etc.)
├── services/
│ ├── pdf.ts (Real PDF generation with jsPDF)
│ └── storage.ts (localStorage for quotes/contacts)
├── utils/
│ └── helpers.ts (formatCurrency, generateQuoteNumber, etc.)
└── public/
├── manifest.json (PWA manifest for iPhone install)
├── icon-192.png (PWA icon)
├── icon-512.png (PWA icon)
└── apple-icon-180.png (Apple touch icon)Features ready:
Where to paste your App.jsx:
Open /components/configurator.tsx - there's a clearly marked section at the top where you should paste your existing React code. The file includes comments showing exactly where to integrate your:
q object)Paste your App.jsx code here and I'll help you integrate it with the new infrastructure.
import { useState } from "react"; // ─── utils ──────────────────────────────────────────────────────────────────── const p = (v) => Math.round(v * 1.05 * 100) / 100; const fmt = (n) => (n ?? 0).toLocaleString("it-IT", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const uid = () => `IB-${new Date().getFullYear()}-${String(Math.floor(Math.random() * 1000)).padStart(3, "0")}`; // ─── schede tecniche ────────────────────────────────────────────────────────── const TECH = { AMB: { titolo:"i.Blue Ambience", garanzia:"20 anni", badge:"ty", righe:[ ["Struttura","Autoportante in pannelli prefabbricati nervati in acciaio zincato di prima qualità"], ["Nervature","Nervature longitudinali → resistenza nettamente superiore a pareti lisce"], ["Protezione","Spesso strato a base di zinco per ottima resistenza alla corrosione"], ["Contrafforti","Acciaio zincato, ancorati con piastre regolabili alla base in cls armato"], ["Pannello int.","Steeline: lamiera zincata pre-verniciata epossidica beige sp.60/100 mm, resistente agli acidi"], ["Membrana","PVC armato sp. 1,5–1,8 mm (Elbtal)"], ["Altezze sk.","H 1200 – 1400 – 1500 mm"], ["Normativa","Verificata staticamente in tutte le zone sismiche – NTC 2018 (D.M. 17/01/2018)"], ]}, INX: { titolo:"i.Blue Inox", garanzia:"30 anni", badge:"tg", righe:[ ["Struttura","Pannelli modulari nervati in acciaio inossidabile UNI EN 10088-4"], ["Nervature","Nervature longitudinali → resistenza superiore vs pareti lisce convenzionali"], ["Materiale","Acciaio inox: nessun rivestimento anticorrosione aggiuntivo richiesto"], ["Contrafforti","Acciaio inox, piastre di fissaggio inox alla base in cls armato"], ["Pannello int.","Steeline: lamiera zincata pre-verniciata epossidica beige sp.60/100 mm"], ["Membrana","PVC armato sp. 1,5–1,8 mm (Elbtal)"], ["Versatilità","Ideale per ambienti difficili: costiero, indoor, alta umidità, acque aggressive"], ["Normativa","Verificata staticamente in tutte le zone sismiche – NTC 2018"], ]}, SHD: { titolo:"Super HD", garanzia:"Marcata CE", badge:"tb", righe:[ ["Struttura","La più avanzata tecnologia autoportante in pannelli LISCI prefabbricati in acciaio"], ["Versioni","Acciaio ZINCATO o BIPLASTIFICATO (doppio strato plastico di protezione)"], ["Pannello","Spessore 2 mm, lunghezza 1000 mm – sistema completamente modulare"], ["Aggancio","Incastro maschio/femmina brevettato: maggiore resistenza e velocità di posa"], ["Viteria ridotta","3 viti per pannello sk. / 5 viti per bordo sfioro – installazione rapida"], ["Regolazione","Bussole di regolazione alla base + piastre sul traverso del contrafforte"], ["Altezze","H 1200 – 1350 – 1500 mm"], ["Certificazioni","Marcata CE – UNI EN 1090-1:2012 • CPR 305/2011 • NTC 2018 • DoP disponibile"], ]}, }; const getTech = (cod) => !cod ? null : cod.startsWith("AMB") ? TECH.AMB : cod.startsWith("INX") ? TECH.INX : cod.startsWith("SHD") ? TECH.SHD : null; // ─── STRUTTURE – Listino 2025 +5% ──────────────────────────────────────────── const STR = { "3x6_sk": {label:"3×6 m — Skimmer", dim:"3×6 m", perim:18,area:18, tipo:"sk",items:[ {cod:"AMB-3x6-120",desc:"Ambience – H 1200 mm",price:p(8055)}, {cod:"AMB-3x6-140",desc:"Ambience – H 1400 mm",price:p(8795)}, {cod:"AMB-3x6-150",desc:"Ambience – H 1500 mm",price:p(11080)}, {cod:"INX-3x6-120",desc:"Inox – H 1200 mm",price:p(12545)}, {cod:"INX-3x6-140",desc:"Inox – H 1400 mm",price:p(14900)}, {cod:"SHD-3x6-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(6171)}, {cod:"SHD-3x6-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(8038)}, {cod:"SHD-3x6-Z150",desc:"Super HD Zincata – H 1500 mm",price:p(8038)}, {cod:"SHD-3x6-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(10442)}, {cod:"SHD-3x6-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(12800)}, {cod:"SHD-3x6-P150",desc:"Super HD Plastificata – H 1500 mm",price:p(12800)}, ]}, "3x7_sk": {label:"3×7 m — Skimmer", dim:"3×7 m", perim:20,area:21, tipo:"sk",items:[ {cod:"AMB-3x7-120",desc:"Ambience – H 1200 mm",price:p(8620)}, {cod:"AMB-3x7-140",desc:"Ambience – H 1400 mm",price:p(9590)}, {cod:"AMB-3x7-150",desc:"Ambience – H 1500 mm",price:p(11830)}, {cod:"INX-3x7-120",desc:"Inox – H 1200 mm",price:p(14100)}, {cod:"INX-3x7-140",desc:"Inox – H 1400 mm",price:p(16760)}, {cod:"SHD-3x7-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(6853)}, {cod:"SHD-3x7-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(8909)}, {cod:"SHD-3x7-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(11565)}, {cod:"SHD-3x7-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(14183)}, ]}, "4x8_sk": {label:"4×8 m — Skimmer", dim:"4×8 m", perim:24,area:32, tipo:"sk",items:[ {cod:"AMB-4x8-120",desc:"Ambience – H 1200 mm",price:p(10750)}, {cod:"AMB-4x8-140",desc:"Ambience – H 1400 mm",price:p(11685)}, {cod:"AMB-4x8-150",desc:"Ambience – H 1500 mm",price:p(14745)}, {cod:"INX-4x8-120",desc:"Inox – H 1200 mm",price:p(15795)}, {cod:"INX-4x8-140",desc:"Inox – H 1400 mm",price:p(18750)}, {cod:"SHD-4x8-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(8054)}, {cod:"SHD-4x8-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(10479)}, {cod:"SHD-4x8-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(13658)}, {cod:"SHD-4x8-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(16774)}, ]}, "4x9_sk": {label:"4×9 m — Skimmer", dim:"4×9 m", perim:26,area:36, tipo:"sk",items:[ {cod:"AMB-4x9-120",desc:"Ambience – H 1200 mm",price:p(11055)}, {cod:"AMB-4x9-140",desc:"Ambience – H 1400 mm",price:p(11960)}, {cod:"AMB-4x9-150",desc:"Ambience – H 1500 mm",price:p(15165)}, {cod:"INX-4x9-120",desc:"Inox – H 1200 mm",price:p(16945)}, {cod:"INX-4x9-140",desc:"Inox – H 1400 mm",price:p(20120)}, {cod:"SHD-4x9-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(8871)}, {cod:"SHD-4x9-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(11512)}, {cod:"SHD-4x9-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(14994)}, {cod:"SHD-4x9-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(18390)}, ]}, "4x10_sk": {label:"4×10 m — Skimmer",dim:"4×10 m",perim:28,area:40, tipo:"sk",items:[ {cod:"AMB-4x10-120",desc:"Ambience – H 1200 mm",price:p(12170)}, {cod:"AMB-4x10-140",desc:"Ambience – H 1400 mm",price:p(13190)}, {cod:"AMB-4x10-150",desc:"Ambience – H 1500 mm",price:p(16250)}, {cod:"INX-4x10-120",desc:"Inox – H 1200 mm",price:p(18060)}, {cod:"INX-4x10-140",desc:"Inox – H 1400 mm",price:p(21360)}, {cod:"SHD-4x10-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(9518)}, {cod:"SHD-4x10-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(12361)}, {cod:"SHD-4x10-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(16117)}, {cod:"SHD-4x10-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(19773)}, ]}, "5x10_sk": {label:"5×10 m — Skimmer",dim:"5×10 m",perim:30,area:50, tipo:"sk",items:[ {cod:"AMB-5x10-120",desc:"Ambience – H 1200 mm",price:p(12965)}, {cod:"AMB-5x10-140",desc:"Ambience – H 1400 mm",price:p(14090)}, {cod:"AMB-5x10-150",desc:"Ambience – H 1500 mm",price:p(17310)}, {cod:"INX-5x10-120",desc:"Inox – H 1200 mm",price:p(19120)}, {cod:"INX-5x10-140",desc:"Inox – H 1400 mm",price:p(22695)}, {cod:"SHD-5x10-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(10293)}, {cod:"SHD-5x10-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(13338)}, {cod:"SHD-5x10-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(17210)}, {cod:"SHD-5x10-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(21126)}, ]}, "5x12_sk": {label:"5×12 m — Skimmer",dim:"5×12 m",perim:34,area:60, tipo:"sk",items:[ {cod:"AMB-5x12-120",desc:"Ambience – H 1200 mm",price:p(14350)}, {cod:"AMB-5x12-140",desc:"Ambience – H 1400 mm",price:p(15950)}, {cod:"AMB-5x12-150",desc:"Ambience – H 1500 mm",price:p(20975)}, {cod:"INX-5x12-120",desc:"Inox – H 1200 mm",price:p(23380)}, {cod:"INX-5x12-140",desc:"Inox – H 1400 mm",price:p(27735)}, {cod:"SHD-5x12-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(12258)}, {cod:"SHD-5x12-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(15894)}, {cod:"SHD-5x12-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(20609)}, {cod:"SHD-5x12-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(25285)}, ]}, "6x12_sk": {label:"6×12 m — Skimmer",dim:"6×12 m",perim:36,area:72, tipo:"sk",items:[ {cod:"AMB-6x12-120",desc:"Ambience – H 1200 mm",price:p(15155)}, {cod:"AMB-6x12-140",desc:"Ambience – H 1400 mm",price:p(16425)}, {cod:"AMB-6x12-150",desc:"Ambience – H 1500 mm",price:p(21330)}, {cod:"INX-6x12-120",desc:"Inox – H 1200 mm",price:p(24440)}, {cod:"INX-6x12-140",desc:"Inox – H 1400 mm",price:p(29015)}, {cod:"SHD-6x12-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(13043)}, {cod:"SHD-6x12-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(16898)}, {cod:"SHD-6x12-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(21710)}, {cod:"SHD-6x12-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(26665)}, ]}, "7x14_sk": {label:"7×14 m — Skimmer",dim:"7×14 m",perim:42,area:98, tipo:"sk",items:[ {cod:"AMB-7x14-120",desc:"Ambience – H 1200 mm",price:p(17950)}, {cod:"AMB-7x14-140",desc:"Ambience – H 1400 mm",price:p(19965)}, {cod:"AMB-7x14-150",desc:"Ambience – H 1500 mm",price:p(25170)}, {cod:"INX-7x14-120",desc:"Inox – H 1200 mm",price:p(27260)}, {cod:"INX-7x14-140",desc:"Inox – H 1400 mm",price:p(31780)}, {cod:"SHD-7x14-Z120",desc:"Super HD Zincata – H 1200 mm",price:p(15037)}, {cod:"SHD-7x14-Z135",desc:"Super HD Zincata – H 1350 mm",price:p(19481)}, {cod:"SHD-7x14-P120",desc:"Super HD Plastificata – H 1200 mm",price:p(25102)}, {cod:"SHD-7x14-P135",desc:"Super HD Plastificata – H 1350 mm",price:p(30817)}, ]}, "4x8_sf": {label:"4×8 m — Sfioro", dim:"4×8 m", perim:24,area:32, tipo:"sf",items:[ {cod:"AMB-4x8SF-120",desc:"Ambience Sfioro – H 1200 mm",price:p(18815)}, {cod:"AMB-4x8SF-140",desc:"Ambience Sfioro – H 1400 mm",price:p(19735)}, {cod:"INX-4x8SF-120",desc:"Inox Sfioro – H 1200 mm",price:p(26780)}, {cod:"INX-4x8SF-140",desc:"Inox Sfioro – H 1400 mm",price:p(30270)}, {cod:"SHD-4x8SF-Z120",desc:"Super HD Zincata Sfioro – H 1200 mm",price:p(18754)}, {cod:"SHD-4x8SF-Z135",desc:"Super HD Zincata Sfioro – H 1350 mm",price:p(21292)}, {cod:"SHD-4x8SF-P120",desc:"Super HD Plastificata Sfioro – H 1200 mm",price:p(29699)}, {cod:"SHD-4x8SF-P135",desc:"Super HD Plastificata Sfioro – H 1350 mm",price:p(32968)}, ]}, "5x10_sf": {label:"5×10 m — Sfioro", dim:"5×10 m",perim:30,area:50, tipo:"sf",items:[ {cod:"AMB-5x10SF-120",desc:"Ambience Sfioro – H 1200 mm",price:p(22730)}, {cod:"AMB-5x10SF-140",desc:"Ambience Sfioro – H 1400 mm",price:p(23875)}, {cod:"INX-5x10SF-120",desc:"Inox Sfioro – H 1200 mm",price:p(32070)}, {cod:"INX-5x10SF-140",desc:"Inox Sfioro – H 1400 mm",price:p(36230)}, {cod:"SHD-5x10SF-Z120",desc:"Super HD Zincata Sfioro – H 1200 mm",price:p(22490)}, {cod:"SHD-5x10SF-Z135",desc:"Super HD Zincata Sfioro – H 1350 mm",price:p(25533)}, {cod:"SHD-5x10SF-P120",desc:"Super HD Plastificata Sfioro – H 1200 mm",price:p(35543)}, {cod:"SHD-5x10SF-P135",desc:"Super HD Plastificata Sfioro – H 1350 mm",price:p(39461)}, ]}, "6x12_sf": {label:"6×12 m — Sfioro", dim:"6×12 m",perim:36,area:72, tipo:"sf",items:[ {cod:"AMB-6x12SF-120",desc:"Ambience Sfioro – H 1200 mm",price:p(26475)}, {cod:"AMB-6x12SF-140",desc:"Ambience Sfioro – H 1400 mm",price:p(27785)}, {cod:"INX-6x12SF-120",desc:"Inox Sfioro – H 1200 mm",price:p(39795)}, {cod:"INX-6x12SF-140",desc:"Inox Sfioro – H 1400 mm",price:p(45100)}, {cod:"SHD-6x12SF-Z120",desc:"Super HD Zincata Sfioro – H 1200 mm",price:p(27652)}, {cod:"SHD-6x12SF-Z135",desc:"Super HD Zincata Sfioro – H 1350 mm",price:p(31503)}, {cod:"SHD-6x12SF-P120",desc:"Super HD Plastificata Sfioro – H 1200 mm",price:p(43677)}, {cod:"SHD-6x12SF-P135",desc:"Super HD Plastificata Sfioro – H 1350 mm",price:p(48634)}, ]}, }; // ─── RIVESTIMENTI – prezzi fondo piatto, Listino 2025 pag. 22-33 ───────────── const RIV = { "3x6_sk": {G:p(2071),S:p(2553),M:p(4197),T:p(5055)}, "3x7_sk": {G:p(2108),S:p(2590),M:p(4197),T:p(5055)}, "4x8_sk": {G:p(2741),S:p(3385),M:p(4234),T:p(5092)}, "4x9_sk": {G:p(3374),S:p(4179),M:p(6238),T:p(7589)}, "4x10_sk": {G:p(3374),S:p(4179),M:p(6238),T:p(7589)}, "5x10_sk": {G:p(4062),S:p(5027),M:p(6309),T:p(7589)}, "5x12_sk": {G:p(4732),S:p(5859),M:p(8351),T:p(10068)}, "6x12_sk": {G:p(4732),S:p(5859),M:p(8351),T:p(10068)}, "7x14_sk": {G:p(6036),S:p(7484),M:p(10392),T:p(12547)}, "4x8_sf": {G:p(3263),S:p(3907),M:p(4756),T:p(5614)}, "5x10_sf": {G:p(5218),S:p(6183),M:p(7464),T:p(8744)}, "6x12_sf": {G:p(5851),S:p(6977),M:p(9469),T:p(11186)}, }; const RIV_OPTS = [ {key:"G",label:"Elbtal Gloobe 1,5 mm", note:"PVC armato calandrato, maglia rinforzo interna – standard"}, {key:"S",label:"Elbtal Supra Laccata 1,5 mm",note:"PVC armato con finitura laccata – estetica premium"}, {key:"M",label:"Elbtal Elite Motion 3D 1,8 mm",note:"Esente ftalati, massima resistenza allo sbiancamento"}, {key:"T",label:"Elbtal Solid Stone 3D 1,8 mm",note:"Texture pietre realistiche, toni naturali"}, ]; const BORDI = { "3x6_sk": {cod:"BRD-3x6", desc:"Bordi Toro Bianchi Sabbiati – 3×6 m", price:p(2021)}, "3x7_sk": {cod:"BRD-3x7", desc:"Bordi Toro Bianchi Sabbiati – 3×7 m", price:p(2197)}, "4x8_sk": {cod:"BRD-4x8", desc:"Bordi Toro Bianchi Sabbiati – 4×8 m", price:p(2549)}, "4x9_sk": {cod:"BRD-4x9", desc:"Bordi Toro Bianchi Sabbiati – 4×9 m", price:p(2725)}, "4x10_sk": {cod:"BRD-4x10", desc:"Bordi Toro Bianchi Sabbiati – 4×10 m", price:p(2901)}, "5x10_sk": {cod:"BRD-5x10", desc:"Bordi Toro Bianchi Sabbiati – 5×10 m", price:p(3077)}, "5x12_sk": {cod:"BRD-5x12", desc:"Bordi Toro Bianchi Sabbiati – 5×12 m", price:p(3429)}, "6x12_sk": {cod:"BRD-6x12", desc:"Bordi Toro Bianchi Sabbiati – 6×12 m", price:p(3605)}, "7x14_sk": {cod:"BRD-7x14", desc:"Bordi Toro Bianchi Sabbiati – 7×14 m", price:p(4290)}, }; const CIRC = { "3x6_sk": [{cod:"C-3x6-R", desc:"Circolazione Sk. – 2 bocc.+1 skimmer+2 prese fondo – tubo Revix", price:p(1206)},{cod:"C-3x6-B", desc:"Circolazione Sk. – 2 bocc.+1 skimmer+2 prese fondo – tubo B-Active",price:p(1626)}], "3x7_sk": [{cod:"C-3x7-R", desc:"Circolazione Sk. – 2 bocc.+1 skimmer+2 prese fondo – tubo Revix", price:p(1206)},{cod:"C-3x7-B", desc:"Circolazione Sk. – 2 bocc.+1 skimmer+2 prese fondo – tubo B-Active",price:p(1626)}], "4x8_sk": [{cod:"C-4x8-R", desc:"Circolazione Sk. – 2 bocc.+1 skimmer+2 prese fondo – tubo Revix", price:p(1206)},{cod:"C-4x8-B", desc:"Circolazione Sk. – 2 bocc.+1 skimmer+2 prese fondo – tubo B-Active",price:p(1626)}], "4x9_sk": [{cod:"C-4x9-R", desc:"Circolazione Sk. – 2 bocc.+2 skimmer+2 prese fondo – tubo Revix", price:p(1605)},{cod:"C-4x9-B", desc:"Circolazione Sk. – 2 bocc.+2 skimmer+2 prese fondo – tubo B-Active",price:p(2025)}], "4x10_sk": [{cod:"C-4x10-R",desc:"Circolazione Sk. – 2 bocc.+2 skimmer+2 prese fondo – tubo Revix", price:p(1605)},{cod:"C-4x10-B",desc:"Circolazione Sk. – 2 bocc.+2 skimmer+2 prese fondo – tubo B-Active",price:p(2025)}], "5x10_sk": [{cod:"C-5x10-R",desc:"Circolazione Sk. – 2 bocc.+2 skimmer+2 prese fondo – tubo Revix", price:p(1605)},{cod:"C-5x10-B",desc:"Circolazione Sk. – 2 bocc.+2 skimmer+2 prese fondo – tubo B-Active",price:p(2025)}], "5x12_sk": [{cod:"C-5x12-R",desc:"Circolazione Sk. – 4 bocc.+3 skimmer+2 prese fondo – tubo Revix", price:p(2543)},{cod:"C-5x12-B",desc:"Circolazione Sk. – 4 bocc.+3 skimmer+2 prese fondo – tubo B-Active",price:p(3245)}], "6x12_sk": [{cod:"C-6x12-R",desc:"Circolazione Sk. – 4 bocc.+3 skimmer+2 prese fondo – tubo Revix", price:p(2543)},{cod:"C-6x12-B",desc:"Circolazione Sk. – 4 bocc.+3 skimmer+2 prese fondo – tubo B-Active",price:p(3245)}], "7x14_sk": [{cod:"C-7x14-R",desc:"Circolazione Sk. – 4 bocc.+4 skimmer+2 prese fondo – tubo Revix", price:p(3106)},{cod:"C-7x14-B",desc:"Circolazione Sk. – 4 bocc.+4 skimmer+2 prese fondo – tubo B-Active",price:p(3949)}], "4x8_sf": [{cod:"C-4x8SF-R",desc:"Circolazione Sf. – 2 bocc.pav.+1 aspira+2 prese fondo – tubo Revix",price:p(849)},{cod:"C-4x8SF-B",desc:"Circolazione Sf. – 2 bocc.pav.+1 aspira+2 prese fondo – tubo B-Active",price:p(1269)}], "5x10_sf": [{cod:"C-5x10SF-R",desc:"Circolazione Sf. – 3 bocc.pav.+1 aspira+2 prese fondo – tubo Revix",price:p(900)},{cod:"C-5x10SF-B",desc:"Circolazione Sf. – 3 bocc.pav.+1 aspira+2 prese fondo – tubo B-Active",price:p(1320)}], "6x12_sf": [{cod:"C-6x12SF-R",desc:"Circolazione Sf. – 4 bocc.pav.+1 aspira+2 prese fondo – tubo Revix",price:p(1204)},{cod:"C-6x12SF-B",desc:"Circolazione Sf. – 4 bocc.pav.+1 aspira+2 prese fondo – tubo B-Active",price:p(2532)}], }; const FILTR = { "3x6_sk": [{cod:"GL10A-P",desc:"GloobeLine 10 m³/h – Pozzetto Gloobe preassemblato",price:p(3509)},{cod:"GR10A-P",desc:"GreenLine 10 m³/h – Pozzetto Rotax preassemblato",price:p(5769)},{cod:"BL10A-P",desc:"BlackLine 10 m³/h – Pozzetto Rotax preassemblato",price:p(6209)},{cod:"GL10A-A",desc:"GloobeLine 10 m³/h – Impianto da assemblare",price:p(1753)},{cod:"GR10A-A",desc:"GreenLine 10 m³/h – Impianto da assemblare",price:p(2758)},{cod:"BL10A-A",desc:"BlackLine 10 m³/h – Impianto da assemblare",price:p(3374)}], "3x7_sk": [{cod:"GL10B-P",desc:"GloobeLine 10 m³/h – Pozzetto Gloobe preassemblato",price:p(3509)},{cod:"GR10B-P",desc:"GreenLine 10 m³/h – Pozzetto Rotax preassemblato",price:p(5769)},{cod:"BL10B-P",desc:"BlackLine 10 m³/h – Pozzetto Rotax preassemblato",price:p(6209)},{cod:"GL10B-A",desc:"GloobeLine 10 m³/h – Impianto da assemblare",price:p(1753)},{cod:"GR10B-A",desc:"GreenLine 10 m³/h – Impianto da assemblare",price:p(2758)},{cod:"BL10B-A",desc:"BlackLine 10 m³/h – Impianto da assemblare",price:p(3374)}], "4x8_sk": [{cod:"GL10C-P",desc:"GloobeLine 10 m³/h – Pozzetto Gloobe preassemblato",price:p(3509)},{cod:"GR10C-P",desc:"GreenLine 10 m³/h – Pozzetto Rotax preassemblato",price:p(5769)},{cod:"BL10C-P",desc:"BlackLine 10 m³/h – Pozzetto Rotax preassemblato",price:p(6209)},{cod:"GL10C-A",desc:"GloobeLine 10 m³/h – Impianto da assemblare",price:p(1753)},{cod:"GR10C-A",desc:"GreenLine 10 m³/h – Impianto da assemblare",price:p(2758)},{cod:"BL10C-A",desc:"BlackLine 10 m³/h – Impianto da assemblare",price:p(3374)}], "4x9_sk": [{cod:"GL15A-P",desc:"GloobeLine 15 m³/h – Pozzetto Gloobe preassemblato",price:p(4103)},{cod:"GR16A-P",desc:"GreenLine 16 m³/h – Pozzetto Rotax preassemblato",price:p(6050)},{cod:"BL15A-P",desc:"BlackLine 15 m³/h – Pozzetto Rotax preassemblato",price:p(6530)},{cod:"GL15A-A",desc:"GloobeLine 15 m³/h – Impianto da assemblare",price:p(2085)},{cod:"GR16A-A",desc:"GreenLine 16 m³/h – Impianto da assemblare",price:p(3176)},{cod:"BL15A-A",desc:"BlackLine 15 m³/h – Impianto da assemblare",price:p(3749)}], "4x10_sk": [{cod:"GL15B-P",desc:"GloobeLine 15 m³/h – Pozzetto Gloobe preassemblato",price:p(4103)},{cod:"GR16B-P",desc:"GreenLine 16 m³/h – Pozzetto Rotax preassemblato",price:p(6050)},{cod:"BL15B-P",desc:"BlackLine 15 m³/h – Pozzetto Rotax preassemblato",price:p(6530)},{cod:"GL15B-A",desc:"GloobeLine 15 m³/h – Impianto da assemblare",price:p(2085)},{cod:"GR16B-A",desc:"GreenLine 16 m³/h – Impianto da assemblare",price:p(3176)},{cod:"BL15B-A",desc:"BlackLine 15 m³/h – Impianto da assemblare",price:p(3749)}], "5x10_sk": [{cod:"GL15C-P",desc:"GloobeLine 15 m³/h – Pozzetto Gloobe preassemblato",price:p(4103)},{cod:"GR16C-P",desc:"GreenLine 16 m³/h – Pozzetto Rotax preassemblato",price:p(6050)},{cod:"BL15C-P",desc:"BlackLine 15 m³/h – Pozzetto Rotax preassemblato",price:p(6530)},{cod:"GL15C-A",desc:"GloobeLine 15 m³/h – Impianto da assemblare",price:p(2085)},{cod:"GR16C-A",desc:"GreenLine 16 m³/h – Impianto da assemblare",price:p(3176)},{cod:"BL15C-A",desc:"BlackLine 15 m³/h – Impianto da assemblare",price:p(3749)}], "5x12_sk": [{cod:"GL22A-P",desc:"GloobeLine 22 m³/h – Pozzetto Gloobe preassemblato",price:p(4948)},{cod:"BL23A-P",desc:"BlackLine 23 m³/h – Pozzetto Rotax preassemblato",price:p(7823)},{cod:"GL22A-A",desc:"GloobeLine 22 m³/h – Impianto da assemblare",price:p(2983)},{cod:"GR23A-A",desc:"GreenLine 23 m³/h – Impianto da assemblare",price:p(4046)},{cod:"BL22A-A",desc:"BlackLine 22 m³/h – Impianto da assemblare",price:p(4942)}], "6x12_sk": [{cod:"GL22B-P",desc:"GloobeLine 22 m³/h – Pozzetto Gloobe preassemblato",price:p(4948)},{cod:"BL23B-P",desc:"BlackLine 23 m³/h – Pozzetto Rotax preassemblato",price:p(7823)},{cod:"GL22B-A",desc:"GloobeLine 22 m³/h – Impianto da assemblare",price:p(2983)},{cod:"GR23B-A",desc:"GreenLine 23 m³/h – Impianto da assemblare",price:p(4046)},{cod:"BL22B-A",desc:"BlackLine 22 m³/h – Impianto da assemblare",price:p(4942)}], "7x14_sk": [{cod:"GL32A-P",desc:"GloobeLine 32 m³/h – Pozzetto VTR Pool's preassemblato",price:p(7871)},{cod:"GR32A-P",desc:"GreenLine 32 m³/h – Pozzetto VTR Pool's preassemblato",price:p(10497)},{cod:"BL33A-P",desc:"BlackLine 33 m³/h – Pozzetto VTR Pool's preassemblato",price:p(11687)},{cod:"GL32A-A",desc:"GloobeLine 32 m³/h – Impianto da assemblare",price:p(3367)},{cod:"GR32A-A",desc:"GreenLine 32 m³/h – Impianto da assemblare",price:p(4826)},{cod:"BL33A-A",desc:"BlackLine 33 m³/h – Impianto da assemblare",price:p(5890)}], "4x8_sf": [{cod:"GL10D-A",desc:"GloobeLine 10 m³/h – Impianto da assemblare",price:p(5677)},{cod:"GR10D-A",desc:"GreenLine 10 m³/h – Impianto da assemblare",price:p(6322)},{cod:"BL10D-A",desc:"BlackLine 10 m³/h – Impianto da assemblare",price:p(6934)}], "5x10_sf": [{cod:"GL15D-A",desc:"GloobeLine 15 m³/h – Impianto da assemblare",price:p(6688)},{cod:"GR16D-A",desc:"GreenLine 16 m³/h – Impianto da assemblare",price:p(7410)},{cod:"BL15D-A",desc:"BlackLine 15 m³/h – Impianto da assemblare",price:p(7974)}], "6x12_sf": [{cod:"GL22C-A",desc:"GloobeLine 22 m³/h – Impianto da assemblare",price:p(7556)},{cod:"GR23C-A",desc:"GreenLine 23 m³/h – Impianto da assemblare",price:p(8226)},{cod:"BL22C-A",desc:"BlackLine 22 m³/h – Impianto da assemblare",price:p(9093)}], }; const LUCI = [ {cod:"LBW1",desc:"Luce Bianca Luxline 1150 lm – 1 faro (c/nicchia+trasf.)",price:p(554)}, {cod:"LBW2",desc:"Luce Bianca Luxline 1150 lm – 2 fari",price:p(1032)}, {cod:"LBW3",desc:"Luce Bianca Luxline 1150 lm – 3 fari",price:p(1525)}, {cod:"LBW4",desc:"Luce Bianca Luxline 1150 lm – 4 fari",price:p(2003)}, {cod:"LRGB1",desc:"Luce RGB Luxline 1050 lm – 1 faro (c/nicchia+trasf.+telec.)",price:p(946)}, {cod:"LRGB2",desc:"Luce RGB Luxline 1050 lm – 2 fari",price:p(1578)}, {cod:"LRGB3",desc:"Luce RGB Luxline 1050 lm – 3 fari",price:p(2225)}, {cod:"LRGB4",desc:"Luce RGB Luxline 1050 lm – 4 fari",price:p(2857)}, {cod:"LADAPT",desc:"Piastra adattamento faro struttura i.Blue (cad.)",price:p(60)}, ]; const SCALINE = [ {cod:"SCL-RA120",desc:"Scala Rettangolare 1,5×2 m – Ambience H1200 Skimmer",price:p(2145)}, {cod:"SCL-RA140",desc:"Scala Rettangolare 1,5×2 m – Ambience H1400 Skimmer",price:p(2385)}, {cod:"SCL-RI120",desc:"Scala Rettangolare 1,5×2 m – Inox H1200 Skimmer",price:p(2805)}, {cod:"SCL-ROA120",desc:"Scala Romana r.1,5 m – Ambience H1200 Skimmer",price:p(2575)}, {cod:"SCL-ROA140",desc:"Scala Romana r.1,5 m – Ambience H1400 Skimmer",price:p(2740)}, {cod:"SCL-ROZ120",desc:"Scala Romana r.1,5 m – Super HD Zincata H1200",price:p(1900)}, {cod:"SOFTBLUE",desc:"Tappetino Softblue fondo piscina (€/m²)",price:p(53)}, ]; const OPT = [ {cod:"SKM-AGG",desc:"Kit Skimmer Sfioratore aggiuntivo",price:p(447)}, {cod:"BOCC-MAN",desc:"Kit Bocchetta Mandata Parete aggiuntiva",price:p(60)}, {cod:"BOCC-ASP",desc:"Kit Bocchetta Aspirafango Parete aggiuntiva",price:p(60)}, {cod:"REG-LIV",desc:"Kit Regolatore di Livello",price:p(163)}, {cod:"COLL-SF4",desc:"Collettore Sfioro n.4 Pilette Ø125 mm",price:p(1681)}, {cod:"COLL-SF6",desc:"Collettore Sfioro n.6 Pilette Ø125 mm",price:p(2227)}, {cod:"USC-POZ50",desc:"Uscita Pozzetto Ø50 mm",price:p(105)}, {cod:"USC-POZ63",desc:"Uscita Pozzetto Ø63 mm",price:p(111)}, {cod:"USC-IMP50",desc:"Uscita Impianto Ø50 mm",price:p(57)}, {cod:"USC-IMP63",desc:"Uscita Impianto Ø63 mm",price:p(64)}, {cod:"PHUNG-V",desc:"Profilo Hung verticale bianco (barra 2 m)",price:p(20)}, {cod:"PHUNG-FL",desc:"Profilo Hung verticale forme libere (barra 2 m)",price:p(51.60)}, {cod:"PIASTR-ZN",desc:"Piastra zincata regolazione contrafforte",price:p(16.70)}, ]; const ALL = [...Object.values(STR).flatMap(m=>m.items),...LUCI,...SCALINE,...OPT]; const DISC = [0,35,40,45,50,55,60]; const STEPS = ["Cliente","Modello","Struttura","Rivestimento","Idraulica","Filtrazione","Accessori","Preventivo"]; // ─── GENERA PDF ─────────────────────────────────────────────────────────────── // Crea un file HTML autonomo con bottone di stampa interno. // Su PC: si apre in nuova tab (usa il bottone "Stampa/PDF" nella pagina). // Su iOS Safari / Android Chrome: effettua il download del file .html; // l'utente lo apre e usa il menu "Stampa" / "Condividi → Stampa" del browser. function generaPDF(q, lines, lordo, netto, sc, mg, model) { const e = (s="") => String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); const tech = getTech(q.struttura?.cod); const matRows = lines.length === 0 ? `<tr><td colspan="6" style="text-align:center;color:#aaa;padding:18px;">Nessuna voce selezionata</td></tr>` : lines.map((l,i) => ` <tr style="border-bottom:1px solid #eef2f8;"> <td style="padding:6px 8px;color:#aaa;font-size:11px;text-align:center;">${i+1}</td> <td style="padding:6px 8px;font-family:monospace;font-size:10px;color:#1a5aa0;white-space:nowrap;">${e(l.cod)}</td> <td style="padding:6px 8px;font-size:12px;">${e(l.desc)}</td> <td style="padding:6px 8px;text-align:center;font-size:12px;">${l.qty||1}</td> <td style="padding:6px 8px;text-align:right;font-size:12px;">€ ${fmt(l.price)}</td> <td style="padding:6px 8px;text-align:right;font-size:12px;font-weight:700;color:#0b4d91;">€ ${fmt(l.price*(l.qty||1))}</td> </tr>`).join(""); const techBlock = tech ? ` <div style="background:#f0f7ff;border-radius:8px;padding:12px 15px;margin-top:12px;"> <div style="font-weight:800;color:#0b4d91;font-size:12px;margin-bottom:8px;">${e(tech.titolo)} — Garanzia: ${e(tech.garanzia)}</div> <table style="width:100%;font-size:11px;border-collapse:collapse;"> ${tech.righe.map(([k,v])=>`<tr><td style="padding:3px 10px 3px 0;color:#4a6a9a;font-weight:700;white-space:nowrap;vertical-align:top;width:120px;">${e(k)}</td><td style="padding:3px 0;line-height:1.5;">${e(v)}</td></tr>`).join("")} </table> </div>` : ""; const gridTecnico = model ? ` <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:${tech?"0":"4px"};"> ${[["Modello",model.label],["Dimensioni",model.dim],["Perimetro",model.perim+" m"],["Area",model.area+" m²"],["Circolazione",model.tipo==="sf"?"Sfioro":"Skimmer"],["Struttura",(q.struttura?.desc||"—").split("–")[0].trim()]].map(([k,v])=>` <div style="background:#f4f8ff;border-radius:7px;padding:8px 10px;"> <div style="font-size:9px;color:#5a7a9a;font-weight:800;text-transform:uppercase;letter-spacing:.7px;">${k}</div> <div style="font-weight:700;font-size:12px;margin-top:2px;">${e(v)}</div> </div>`).join("")} </div>` : ""; const html = `<!DOCTYPE html> <html lang="it"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Preventivo ${e(q.num)} — i.Blue Piscine</title> <style> *{box-sizing:border-box;margin:0;padding:0;} body{font-family:'Segoe UI',Arial,sans-serif;font-size:13px;color:#1a2a3a;background:#fff;} .wrap{max-width:820px;margin:0 auto;padding:20px 16px;} .box{border:1.5px solid #d0e4f7;border-radius:10px;padding:14px 16px;margin-bottom:14px;} h3{font-size:13px;font-weight:800;color:#0b4d91;margin-bottom:10px;} .btn-print{display:block;width:100%;padding:14px;background:#0b4d91;color:#fff; border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer; margin-bottom:18px;text-align:center;letter-spacing:.2px;} .btn-print:hover{background:#0d62b8;} table{width:100%;border-collapse:collapse;} @media(max-width:600px){ .wrap{padding:12px 10px;} .grid3{grid-template-columns:1fr 1fr!important;} .tbl-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;} table{min-width:500px;} } @media print{ .btn-print{display:none!important;} .wrap{padding:0;max-width:100%;} body{background:#fff;} @page{margin:14mm;} } </style> </head> <body> <div class="wrap"> <button class="btn-print" onclick="window.print()">🖨️ Stampa / Salva come PDF — usa la funzione di stampa del browser</button> <!-- INTESTAZIONE --> <div style="background:linear-gradient(135deg,#0b4d91,#1272c8);color:#fff;border-radius:12px;padding:20px 22px;margin-bottom:14px;"> <div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px;"> <div> <div style="font-size:22px;font-weight:900;letter-spacing:-0.5px;">i.Blue Piscine</div> <div style="font-size:11px;opacity:.8;margin-top:4px;">A&T Europe S.p.A. – Via Solferino 27, 46043 Castiglione delle Stiviere (MN)</div> <div style="font-size:11px;opacity:.65;margin-top:2px;">www.ibluepiscine.it • Tel. 800 713 585 • info@ibluepiscine.it</div> </div> <div style="text-align:right;"> <div style="font-size:13px;font-weight:800;opacity:.9;">PREVENTIVO</div> <div style="font-size:18px;font-weight:900;font-family:monospace;background:rgba(255,255,255,.2);padding:3px 12px;border-radius:7px;margin-top:3px;">${e(q.num)}</div> <div style="font-size:11px;opacity:.8;margin-top:5px;">Data: ${e(q.date)}</div> </div> </div> <div style="margin-top:14px;background:rgba(255,255,255,.12);border-radius:9px;padding:11px 14px;display:grid;grid-template-columns:1fr 1fr;gap:8px;"> ${[["Cliente",q.cliente||"—"],["Azienda",q.azienda||"—"],["Referente",q.referente||"—"],["Contatti",[q.tel,q.email].filter(Boolean).join(" • ")||"—"]].map(([k,v])=>` <div> <div style="font-size:9px;opacity:.7;font-weight:800;text-transform:uppercase;letter-spacing:1px;">${k}</div> <div style="font-weight:700;font-size:13px;margin-top:2px;">${e(v)}</div> </div>`).join("")} </div> </div> <!-- RIEPILOGO TECNICO --> ${model ? `<div class="box"> <h3>📐 Riepilogo Tecnico Piscina</h3> <div class="grid3" style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;">${gridTecnico.match(/<div style="background.*?<\/div>\s*<\/div>/gs)?.join("")||gridTecnico}</div> ${techBlock} </div>` : ""} <!-- ELENCO MATERIALI --> <div class="box"> <h3>📦 Elenco Materiali</h3> <div class="tbl-scroll"> <table> <thead> <tr style="background:#f0f6ff;border-bottom:2px solid #d0e4f7;"> <th style="padding:7px 8px;font-size:10px;color:#5a7a9a;text-transform:uppercase;text-align:center;">#</th> <th style="padding:7px 8px;font-size:10px;color:#5a7a9a;text-transform:uppercase;">Codice</th> <th style="padding:7px 8px;font-size:10px;color:#5a7a9a;text-transform:uppercase;">Descrizione</th> <th style="padding:7px 8px;font-size:10px;color:#5a7a9a;text-transform:uppercase;text-align:center;">Q.</th> <th style="padding:7px 8px;font-size:10px;color:#5a7a9a;text-transform:uppercase;text-align:right;">P.Unit.</th> <th style="padding:7px 8px;font-size:10px;color:#5a7a9a;text-transform:uppercase;text-align:right;">Totale</th> </tr> </thead> <tbody>${matRows}</tbody> </table> </div> </div> <!-- CONDIZIONI ECONOMICHE --> <div class="box"> <h3>💶 Condizioni Economiche</h3> <div style="background:#f4f8ff;border-radius:10px;padding:14px 16px;"> <div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;"> <span style="color:#5a7a9a;">Totale Listino 2025 (+5%)</span> <span style="font-weight:700;">€ ${fmt(lordo)}</span> </div> <div style="display:flex;justify-content:space-between;margin-bottom:12px;font-size:13px;"> <span style="color:#5a7a9a;">Sconto applicato ${sc}%</span> <span style="font-weight:700;color:#dc2626;">− € ${fmt(mg)}</span> </div> <div style="border-top:2px solid #d0e4f7;padding-top:12px;display:flex;justify-content:space-between;align-items:center;"> <span style="font-size:17px;font-weight:900;color:#0b4d91;">TOTALE NETTO</span> <span style="font-size:20px;font-weight:900;color:#0b4d91;">€ ${fmt(netto)}</span> </div> <div style="font-size:10px;color:#7a96b2;margin-top:6px;">IVA esclusa • FCA Castiglione delle Stiviere (MN) – Incoterms® 2020</div> </div> </div> ${q.note ? `<div style="background:#fffbeb;border:1.5px solid #fde68a;border-radius:9px;padding:11px 14px;margin-bottom:14px;"> <div style="font-size:10px;font-weight:800;color:#92400e;margin-bottom:3px;">NOTE</div> <div style="font-size:12px;">${e(q.note)}</div> </div>` : ""} <div style="font-size:10px;color:#7a96b2;line-height:1.9;border-top:1px solid #e2ecf5;padding-top:12px;"> Validità offerta: 30 giorni dalla data di emissione. Prezzi IVA esclusa – FCA Castiglione delle Stiviere (MN).<br> Prezzi calcolati su Listino i.Blue 2025, maggiorati del 5%. Per informazioni: info@ibluepiscine.it • www.ibluepiscine.it </div> </div> </body> </html>`; const blob = new Blob([html], { type: "text/html;charset=utf-8" }); const url = URL.createObjectURL(blob); // Prova prima con window.open (PC/Android Chrome) const win = window.open(url, "_blank"); if (!win || win.closed || typeof win.closed === "undefined") { // Fallback download (iOS Safari, popup bloccati) const a = document.createElement("a"); a.href = url; a.download = `Preventivo-${q.num}.html`; a.style.display = "none"; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 10000); } else { setTimeout(() => URL.revokeObjectURL(url), 60000); } } // ─── CSS mobile-first ───────────────────────────────────────────────────────── const CSS = ` @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap'); *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;} html{font-size:16px;} body{font-family:'DM Sans',sans-serif;background:#eef2f7;min-height:100vh;} /* ── bottoni ── */ .btn{cursor:pointer;border:none;border-radius:10px;font-family:inherit;font-weight:700; transition:background .14s,transform .1s;-webkit-appearance:none;touch-action:manipulation;} .btn-p{background:#0b4d91;color:#fff;padding:14px 22px;font-size:15px;width:100%;} .btn-p:active{transform:scale(.97);} .btn-p:disabled{opacity:.45;cursor:not-allowed;} .btn-s{background:#fff;color:#0b4d91;border:2px solid #0b4d91;padding:13px 20px;font-size:15px;width:100%;} .btn-s:active{transform:scale(.97);} /* ── layout ── */ .card{background:#fff;border-radius:16px;box-shadow:0 2px 14px rgba(11,77,145,.07);padding:18px 16px;margin-bottom:14px;} .label{font-size:11px;font-weight:800;color:#7a96b2;text-transform:uppercase;letter-spacing:1px;display:block;margin-bottom:5px;} .inp{width:100%;border:1.5px solid #d4dde9;border-radius:10px;padding:13px 14px;font-size:16px; font-family:inherit;outline:none;background:#f9fbfd;transition:border-color .13s;-webkit-appearance:none;} .inp:focus{border-color:#0b4d91;background:#fff;} /* ── chip ── */ .chip{border:2px solid #d4dde9;border-radius:12px;padding:13px 14px;cursor:pointer; transition:border-color .13s,background .13s;background:#fff;margin-bottom:8px;touch-action:manipulation;} .chip:active{border-color:#0b4d91;background:#f0f7ff;} .chip.on{border-color:#0b4d91;background:#ebf3fd;} .pr{font-size:16px;font-weight:800;color:#0b4d91;} /* ── tag ── */ .tag{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:700;} .tb{background:#ebf3fd;color:#1e4d9a;} .tg{background:#dcfce7;color:#166534;} .ty{background:#fef3c7;color:#92400e;} /* ── step nav ── */ .snav{display:flex;gap:5px;overflow-x:auto;padding:0 0 6px; -webkit-overflow-scrolling:touch;scrollbar-width:none;} .snav::-webkit-scrollbar{display:none;} .pill{padding:8px 14px;border-radius:20px;font-size:12px;font-weight:700; cursor:pointer;border:none;white-space:nowrap;font-family:inherit;touch-action:manipulation;} .pill-d{background:#0b4d91;color:#fff;} .pill-c{background:#2196f3;color:#fff;} .pill-f{background:#e2ecf5;color:#7a96b2;} /* ── tabella ── */ .tbl{width:100%;border-collapse:collapse;} .tbl th{background:#f4f7fb;color:#7a96b2;font-size:11px;text-transform:uppercase; letter-spacing:.8px;padding:9px 8px;text-align:left;} .tbl td{padding:9px 8px;border-bottom:1px solid #f0f4f8;font-size:12px;} /* ── barra totale fissa ── */ .fbar{position:fixed;bottom:0;left:0;right:0;background:#0b4d91;color:#fff; padding:10px 16px 14px;display:flex;justify-content:space-between;align-items:center; z-index:200;box-shadow:0 -3px 16px rgba(11,77,145,.3);} /* ── notifica ── */ .notif{position:fixed;bottom:70px;left:50%;transform:translateX(-50%); background:#1a3a6a;color:#fff;padding:11px 18px;border-radius:10px;font-size:13px; font-weight:700;z-index:9999;white-space:nowrap;max-width:90vw;text-align:center; box-shadow:0 4px 18px rgba(0,0,0,.22);animation:slideup .3s;} @keyframes slideup{from{opacity:0;transform:translateX(-50%) translateY(8px);}to{opacity:1;transform:translateX(-50%) translateY(0);}} /* ── tech badge ── */ .tbadge{display:inline-flex;align-items:center;gap:5px;background:#f0f6ff; border:1px solid #c8dcf5;border-radius:20px;padding:7px 13px;font-size:12px; font-weight:700;color:#0b4d91;cursor:pointer;touch-action:manipulation;} .tbadge.on{background:#0b4d91;color:#fff;border-color:#0b4d91;} .tpanel{background:#f4f8ff;border:1.5px solid #d0e4f7;border-radius:12px; padding:14px 15px;margin-bottom:14px;font-size:12px;line-height:1.7;} /* ── header ── */ .hdr-btn{padding:8px 11px;font-size:11px;background:rgba(255,255,255,.15); border:1.5px solid rgba(255,255,255,.3);border-radius:8px;color:#fff;cursor:pointer; font-weight:700;font-family:inherit;white-space:nowrap;touch-action:manipulation;} /* ── sconti ── */ .disc-wrap{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:12px;} .disc-btn{padding:11px 12px;font-size:14px;border:2px solid #d4dde9;background:#fff; color:#0b4d91;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit; touch-action:manipulation;flex:1;min-width:52px;text-align:center;} .disc-btn.on{background:#0b4d91;color:#fff;border-color:#0b4d91;} /* ── griglia responsive ── */ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:8px;} .grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;} .grid-auto{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px;} /* ── mobile <600px ── */ @media(max-width:600px){ .card{padding:15px 13px;} .grid2{grid-template-columns:1fr;} .grid3{grid-template-columns:1fr 1fr;} .grid-auto{grid-template-columns:1fr 1fr;} .fbar{padding:8px 14px 12px;} .fbar-label{font-size:10px;} .fbar-val{font-size:13px;} .disc-btn{padding:10px 8px;font-size:13px;} .tbl th,.tbl td{font-size:11px;padding:8px 6px;} } /* ── tablet ≥600px ── */ @media(min-width:600px){ .btn-p,.btn-s{width:auto;padding:12px 24px;} .btn-row{display:flex;justify-content:space-between;gap:10px;margin-top:20px;} } `; // ─── Chip ──────────────────────────────────────────────────────────────────── const Chip = ({ item, active, onClick, badge }) => ( <div className={`chip ${active ? "on" : ""}`} onClick={onClick}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-start", gap:6 }}> <div style={{ flex:1 }}> <div style={{ fontWeight:700, fontSize:13, lineHeight:1.3 }}>{item.desc}</div> <div style={{ fontSize:10, color:"#7a96b2", marginTop:2, fontFamily:"'DM Mono',monospace" }}>{item.cod}</div> </div> {badge && <div style={{ flexShrink:0, marginTop:1 }}>{badge}</div>} </div> <div style={{ marginTop:8 }}><span className="pr">{fmt(item.price)} €</span></div> </div> ); // ─── BtnRow (mobile-stack / desktop-side) ──────────────────────────────────── const BtnRow = ({ onBack, onNext, nextLabel="Avanti →", nextDisabled=false }) => ( <div className="btn-row" style={{ marginTop:20, display:"flex", flexDirection:"column", gap:8 }}> <button className="btn btn-p" disabled={nextDisabled} onClick={onNext}>{nextLabel}</button> {onBack && <button className="btn btn-s" onClick={onBack}>← Indietro</button>} </div> ); // ─── APP ────────────────────────────────────────────────────────────────────── export default function App() { const [step, setStep] = useState(0); const [q, setQ] = useState({ num:uid(), date:new Date().toLocaleDateString("it-IT"), cliente:"", azienda:"", referente:"", tel:"", email:"", note:"", modelKey:"", struttura:null, rivKey:"", bordi:false, circolazione:null, filtrazione:null, extras:[], sconto:0, customSconto:"", }); const [search, setSearch] = useState(""); const [saved, setSaved] = useState([]); const [showArch, setShowArch] = useState(false); const [notif, setNotif] = useState(""); const [openTech, setOpenTech] = useState(""); const upd = (k, v) => setQ(x => ({ ...x, [k]: v })); const notify = (m) => { setNotif(m); setTimeout(() => setNotif(""), 3500); }; // Reset atomico quando si cambia modello const resetModel = (key) => setQ(x => ({ ...x, modelKey:key, struttura:null, rivKey:"", bordi:false, circolazione:null, filtrazione:null, })); const model = STR[q.modelKey]; // ── righe preventivo ── const lines = []; if (q.struttura) lines.push({ ...q.struttura, qty:1 }); if (q.modelKey && q.rivKey && RIV[q.modelKey]) { const pr = RIV[q.modelKey][q.rivKey]; const lb = RIV_OPTS.find(r => r.key === q.rivKey)?.label || ""; lines.push({ cod:`RIV-${q.rivKey}`, desc:`Rivestimento ${lb}`, price:pr, qty:1 }); } if (q.bordi && BORDI[q.modelKey]) lines.push({ ...BORDI[q.modelKey], qty:1 }); if (q.circolazione) lines.push({ ...q.circolazione, qty:1 }); if (q.filtrazione) lines.push({ ...q.filtrazione, qty:1 }); q.extras.forEach(ex => lines.push({ ...ex, qty: ex.qty || 1 })); const lordo = lines.reduce((s, l) => s + l.price * (l.qty || 1), 0); const sc = Number(q.customSconto) || q.sconto; const netto = lordo * (1 - sc / 100); const mg = lordo - netto; const togE = (item) => setQ(x => { const f = x.extras.find(e => e.cod === item.cod); return f ? { ...x, extras: x.extras.filter(e => e.cod !== item.cod) } : { ...x, extras: [...x.extras, { ...item, qty:1 }] }; }); const isE = (cod) => q.extras.some(e => e.cod === cod); const updQ = (cod, qty) => setQ(x => ({ ...x, extras: x.extras.map(e => e.cod === cod ? { ...e, qty: Number(qty) } : e) })); const save = () => { setSaved(s => [ ...s.filter(x => x.num !== q.num), { ...q, _l:lines, _lor:lordo, _net:netto, _sc:sc }, ]); notify("✓ Preventivo salvato"); }; const dup = () => { upd("num", uid()); notify("🔁 Duplicato"); }; const pdf = () => { generaPDF(q, lines, lordo, netto, sc, mg, model); notify("📥 PDF in apertura — usa il bottone Stampa nella pagina"); }; const srch = search.length > 1 ? ALL.filter(i => i.desc.toLowerCase().includes(search.toLowerCase()) || i.cod.toLowerCase().includes(search.toLowerCase())) : []; const tech = getTech(q.struttura?.cod); const showFbar = step > 0 && step < 7 && lines.length > 0; return ( <div style={{ paddingBottom: showFbar ? 68 : 0 }}> <style>{CSS}</style> {/* ── HEADER ── */} <header style={{ background:"linear-gradient(135deg,#0b4d91,#1272c8)", color:"#fff", padding:"10px 14px" }}> <div style={{ maxWidth:960, margin:"0 auto" }}> <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", gap:10 }}> <div style={{ display:"flex", alignItems:"center", gap:10, flex:1, minWidth:0 }}> <div style={{ width:32, height:32, background:"rgba(255,255,255,.2)", borderRadius:8, display:"flex", alignItems:"center", justifyContent:"center", fontSize:17, flexShrink:0 }}>🏊</div> <div style={{ minWidth:0 }}> <div style={{ fontSize:15, fontWeight:800, lineHeight:1.2 }}>i.Blue Configuratore</div> <div style={{ fontSize:10, opacity:.7, marginTop:1 }}>Listino 2025 +5% • IVA esclusa</div> </div> </div> <div style={{ display:"flex", gap:6, flexShrink:0 }}> <button className="hdr-btn" onClick={() => setShowArch(v => !v)}>📁 {saved.length}</button> <button className="hdr-btn" onClick={save}>💾</button> <button className="hdr-btn" onClick={dup}>🔁</button> <button className="hdr-btn" onClick={pdf}>📄 PDF</button> </div> </div> </div> </header> {/* ── ARCHIVIO (bottom sheet mobile) ── */} {showArch && ( <div style={{ position:"fixed", inset:0, background:"rgba(0,0,0,.5)", zIndex:8000, display:"flex", alignItems:"flex-end", justifyContent:"center" }}> <div style={{ background:"#fff", borderRadius:"18px 18px 0 0", padding:"16px 16px 30px", width:"100%", maxWidth:620, maxHeight:"80vh", overflowY:"auto" }}> <div style={{ display:"flex", justifyContent:"space-between", marginBottom:14 }}> <h3 style={{ fontWeight:800, fontSize:16 }}>Preventivi Salvati</h3> <button className="btn" style={{ background:"#fee2e2", color:"#b91c1c", padding:"6px 14px", borderRadius:7, fontSize:13, width:"auto" }} onClick={() => setShowArch(false)}>✕</button> </div> {saved.length === 0 ? <p style={{ color:"#aaa", textAlign:"center", padding:"24px 0" }}>Nessun preventivo salvato</p> : saved.map(sv => ( <div key={sv.num} style={{ border:"1.5px solid #e2ecf5", borderRadius:12, padding:"12px 14px", marginBottom:10 }}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-start", gap:8 }}> <div> <div style={{ fontWeight:700, fontFamily:"'DM Mono',monospace", fontSize:13 }}>{sv.num}</div> <div style={{ fontSize:11, color:"#7a96b2", marginTop:2 }}> {sv.azienda || sv.cliente || "—"} • {sv.date} </div> <div style={{ fontSize:14, fontWeight:800, color:"#0b4d91", marginTop:3 }}> € {fmt(sv._net || 0)} <span style={{ fontSize:11, fontWeight:500, color:"#7a96b2" }}> (sc.{sv._sc || 0}%)</span> </div> </div> <div style={{ display:"flex", flexDirection:"column", gap:6, flexShrink:0 }}> <button className="btn btn-p" style={{ fontSize:12, padding:"8px 14px", width:"auto" }} onClick={() => { setQ(sv); setShowArch(false); notify("📂 Caricato"); }}> Apri </button> <button className="btn" style={{ background:"#fef3c7", color:"#92400e", border:"none", borderRadius:8, padding:"7px 10px", fontSize:11, fontWeight:700, fontFamily:"inherit", cursor:"pointer", width:"auto" }} onClick={() => generaPDF(sv, sv._l||[], sv._lor||0, sv._net||0, sv._sc||0, (sv._lor||0)-(sv._net||0), STR[sv.modelKey])}> 📄 PDF </button> </div> </div> </div> ))} </div> </div> )} {/* ── MAIN ── */} <main style={{ maxWidth:960, margin:"0 auto", padding:"14px 12px" }}> {/* step nav */} <div className="snav" style={{ marginBottom:14 }}> {STEPS.map((s, i) => ( <button key={s} className={`pill ${i < step ? "pill-d" : i === step ? "pill-c" : "pill-f"}`} onClick={() => i <= step && setStep(i)}> {i < step ? "✓ " : `${i + 1}. `}{s} </button> ))} </div> {/* ─── 0: CLIENTE ─── */} {step === 0 && ( <div className="card"> <h2 style={{ fontWeight:800, fontSize:17, color:"#0b4d91", marginBottom:18 }}>📋 Dati Cliente</h2> <div style={{ display:"flex", flexDirection:"column", gap:13 }}> {[ ["num", "N° Preventivo", "text", "off"], ["date", "Data", "text", "off"], ["cliente", "Cliente *", "text", "name"], ["azienda", "Azienda", "text", "organization"], ["referente","Referente", "text", "name"], ["tel", "Telefono", "tel", "tel"], ["email", "E-mail", "email", "email"], ["note", "Note", "text", "off"], ].map(([k, lb, type, autoComplete]) => ( <div key={k}> <label className="label">{lb}</label> <input className="inp" value={q[k]} onChange={e => upd(k, e.target.value)} placeholder={lb} type={type} inputMode={type === "tel" ? "tel" : type === "email" ? "email" : undefined} autoComplete={autoComplete} /> </div> ))} </div> <BtnRow onNext={() => setStep(1)} nextLabel="Avanti → Modello" /> </div> )} {/* ─── 1: MODELLO ─── */} {step === 1 && ( <div className="card"> <h2 style={{ fontWeight:800, fontSize:17, color:"#0b4d91", marginBottom:14 }}>🏊 Modello Piscina</h2> <div className="grid-auto"> {Object.entries(STR).map(([key, m]) => ( <div key={key} className={`chip ${q.modelKey === key ? "on" : ""}`} onClick={() => resetModel(key)}> <div style={{ fontWeight:700, fontSize:13 }}>{m.label}</div> <div style={{ fontSize:11, color:"#7a96b2", marginTop:3 }}> {m.perim} m • {m.area} m² {m.tipo === "sf" && <span className="tag tb" style={{ marginLeft:5 }}>Sfioro</span>} </div> </div> ))} </div> <BtnRow onBack={() => setStep(0)} onNext={() => setStep(2)} nextLabel="Avanti → Struttura" nextDisabled={!q.modelKey} /> </div> )} {/* ─── 2: STRUTTURA ─── */} {step === 2 && model && ( <div className="card"> <h2 style={{ fontWeight:800, fontSize:17, color:"#0b4d91", marginBottom:6 }}>🏗️ Struttura</h2> <div style={{ fontSize:12, color:"#7a96b2", marginBottom:12 }}> {model.label} — Listino 2025 +5% </div> <div style={{ display:"flex", gap:7, flexWrap:"wrap", marginBottom:12 }}> {["AMB","INX","SHD"].map(k => ( <span key={k} className={`tbadge ${openTech === k ? "on" : ""}`} onClick={() => setOpenTech(v => v === k ? "" : k)}> ℹ️ {TECH[k].titolo} </span> ))} </div> {["AMB","INX","SHD"].map(k => openTech === k && ( <div key={k} className="tpanel"> <div style={{ fontWeight:800, color:"#0b4d91", marginBottom:8, fontSize:13 }}> {TECH[k].titolo} <span className={`tag ${TECH[k].badge}`}>{TECH[k].garanzia}</span> </div> {TECH[k].righe.map(([lbl, val]) => ( <div key={lbl} style={{ marginBottom:4 }}> <span style={{ fontWeight:700, color:"#4a6a9a" }}>{lbl}:</span> {val} </div> ))} </div> ))} <div className="grid2"> {model.items.map(item => ( <Chip key={item.cod} item={item} active={q.struttura?.cod === item.cod} onClick={() => upd("struttura", item)} badge={ item.cod.startsWith("AMB") ? <span className="tag ty" style={{ fontSize:9 }}>20 a.</span> : item.cod.startsWith("INX") ? <span className="tag tg" style={{ fontSize:9 }}>30 a.</span> : <span className="tag tb" style={{ fontSize:9 }}>CE</span> } /> ))} </div> {q.struttura && tech && ( <div style={{ marginTop:10, padding:"9px 13px", background:"#f0f7ff", borderRadius:9, fontSize:11, color:"#3a5a8a", lineHeight:1.6 }}> <b>{tech.titolo}</b> — Garanzia {tech.garanzia}<br />{tech.righe[0][1]} </div> )} <BtnRow onBack={() => setStep(1)} onNext={() => setStep(3)} nextLabel="Avanti → Rivestimento" nextDisabled={!q.struttura} /> </div> )} {/* ─── 3: RIVESTIMENTO ─── */} {step === 3 && model && ( <div className="card"> <h2 style={{ fontWeight:800, fontSize:17, color:"#0b4d91", marginBottom:14 }}>🎨 Rivestimento & Bordi</h2> {RIV[q.modelKey] ? ( <div className="grid2"> {RIV_OPTS.map(({ key, label, note }) => { const pr = RIV[q.modelKey]?.[key]; if (!pr) return null; return ( <div key={key} className={`chip ${q.rivKey === key ? "on" : ""}`} onClick={() => upd("rivKey", key)}> <div style={{ fontWeight:700, fontSize:13 }}>{label}</div> <div style={{ fontSize:11, color:"#7a96b2", marginTop:3 }}>{note}</div> <div style={{ marginTop:7 }}><span className="pr">{fmt(pr)} €</span></div> </div> ); })} </div> ) : <p style={{ color:"#aaa" }}>Non disponibile per questo modello.</p>} {BORDI[q.modelKey] && ( <div style={{ marginTop:14 }}> <div style={{ fontWeight:700, fontSize:13, color:"#0b4d91", marginBottom:8 }}>Bordo Perimetrale</div> <div className={`chip ${q.bordi ? "on" : ""}`} onClick={() => upd("bordi", !q.bordi)}> <div style={{ fontWeight:600, fontSize:13 }}>{BORDI[q.modelKey].desc}</div> <div style={{ fontSize:11, color:"#7a96b2", marginTop:2 }}> Toro Bianco Sabbiato – c/angoli e sezioni a raggio </div> <div style={{ marginTop:7, display:"flex", justifyContent:"space-between" }}> <span className="pr">{fmt(BORDI[q.modelKey].price)} €</span> {q.bordi && <span className="tag tg">✓ Incluso</span>} </div> </div> </div> )} <BtnRow onBack={() => setStep(2)} onNext={() => setStep(4)} nextLabel="Avanti → Idraulica" /> </div> )} {/* ─── 4: CIRCOLAZIONE ─── */} {step === 4 && model && ( <div> <div className="card"> <h2 style={{ fontWeight:800, fontSize:17, color:"#0b4d91", marginBottom:14 }}>💧 Circolazione</h2> <div className="grid2"> {(CIRC[q.modelKey] || []).map(item => ( <div key={item.cod} className={`chip ${q.circolazione?.cod === item.cod ? "on" : ""}`} onClick={() => upd("circolazione", item)}> <div style={{ fontWeight:600, fontSize:12 }}>{item.desc}</div> <div style={{ marginTop:7 }}><span className="pr">{fmt(item.price)} €</span></div> </div> ))} </div> </div> <div className="card"> <h3 style={{ fontWeight:700, fontSize:14, color:"#0b4d91", marginBottom:11 }}>Accessori aggiuntivi</h3> <div style={{ display:"flex", flexDirection:"column", gap:7 }}> {OPT.map(item => ( <div key={item.cod} className={`chip ${isE(item.cod) ? "on" : ""}`} style={{ padding:"10px 13px" }} onClick={() => togE(item)}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", gap:8 }}> <span style={{ fontSize:12, fontWeight:600, flex:1 }}>{item.desc}</span> <div style={{ display:"flex", alignItems:"center", gap:8, flexShrink:0 }}> <span style={{ fontSize:13, fontWeight:800, color:"#0b4d91" }}>{fmt(item.price)} €</span> {isE(item.cod) && ( <input type="number" min="1" value={q.extras.find(e => e.cod === item.cod)?.qty || 1} onClick={e => e.stopPropagation()} onChange={e => updQ(item.cod, e.target.value)} style={{ width:48, border:"1.5px solid #0b4d91", borderRadius:7, padding:"3px 5px", fontSize:13, textAlign:"center", fontFamily:"inherit" }} /> )} </div> </div> </div> ))} </div> </div> <BtnRow onBack={() => setStep(3)} onNext={() => setStep(5)} nextLabel="Avanti → Filtrazione" /> </div> )} {/* ─── 5: FILTRAZIONE ─── */} {step === 5 && model && ( <div className="card"> <h2 style={{ fontWeight:800, fontSize:17, color:"#0b4d91", marginBottom:6 }}>⚙️ Filtrazione</h2> <div style={{ fontSize:12, color:"#7a96b2", marginBottom:14 }}> {model.label} — portata calcolata per ricambio ≤ 6h </div> <div className="grid2"> {(FILTR[q.modelKey] || []).map(item => ( <div key={item.cod} className={`chip ${q.filtrazione?.cod === item.cod ? "on" : ""}`} onClick={() => upd("filtrazione", item)}> <div style={{ fontWeight:600, fontSize:12 }}>{item.desc}</div> <div style={{ marginTop:7 }}><span className="pr">{fmt(item.price)} €</span></div> </div> ))} </div> <BtnRow onBack={() => setStep(4)} onNext={() => setStep(6)} nextLabel="Avanti → Accessori" /> </div> )} {/* ─── 6: ACCESSORI ─── */} {step === 6 && ( <div> <div className="card"> <h3 style={{ fontWeight:800, fontSize:15, color:"#0b4d91", marginBottom:12 }}>💡 Illuminazione</h3> <div style={{ display:"flex", flexDirection:"column", gap:7 }}> {LUCI.map(item => ( <div key={item.cod} className={`chip ${isE(item.cod) ? "on" : ""}`} style={{ padding:"10px 13px" }} onClick={() => togE(item)}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", gap:8 }}> <span style={{ fontSize:12, fontWeight:600, flex:1 }}>{item.desc}</span> <div style={{ display:"flex", alignItems:"center", gap:8, flexShrink:0 }}> <span style={{ fontSize:13, fontWeight:800, color:"#0b4d91" }}>{fmt(item.price)} €</span> {isE(item.cod) && <span className="tag tg">✓</span>} </div> </div> </div> ))} </div> </div> <div className="card"> <h3 style={{ fontWeight:800, fontSize:15, color:"#0b4d91", marginBottom:12 }}>🪜 Scalinate & Softblue</h3> <div style={{ display:"flex", flexDirection:"column", gap:7 }}> {SCALINE.map(item => ( <div key={item.cod} className={`chip ${isE(item.cod) ? "on" : ""}`} style={{ padding:"10px 13px" }} onClick={() => togE(item)}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", gap:8 }}> <span style={{ fontSize:12, fontWeight:600, flex:1 }}>{item.desc}</span> <div style={{ display:"flex", alignItems:"center", gap:8, flexShrink:0 }}> <span style={{ fontSize:13, fontWeight:800, color:"#0b4d91" }}> {fmt(item.price)} €{item.cod === "SOFTBLUE" ? "/m²" : ""} </span> {isE(item.cod) && ( <input type="number" min="1" value={q.extras.find(e => e.cod === item.cod)?.qty || 1} onClick={e => e.stopPropagation()} onChange={e => updQ(item.cod, e.target.value)} style={{ width:48, border:"1.5px solid #0b4d91", borderRadius:7, padding:"3px 5px", fontSize:13, textAlign:"center", fontFamily:"inherit" }} /> )} </div> </div> </div> ))} </div> </div> <div className="card"> <h3 style={{ fontWeight:800, fontSize:15, color:"#0b4d91", marginBottom:12 }}>🔍 Cerca Articolo</h3> <input className="inp" placeholder="Codice o parola chiave (es. bocchetta, AMB-4x8…)" value={search} onChange={e => setSearch(e.target.value)} /> {srch.length > 0 && ( <div style={{ marginTop:10, maxHeight:250, overflowY:"auto", border:"1px solid #e2ecf5", borderRadius:10 }}> {srch.slice(0, 30).map(item => ( <div key={item.cod} style={{ display:"flex", justifyContent:"space-between", alignItems:"center", padding:"9px 12px", borderBottom:"1px solid #f4f7fb", gap:8 }}> <div style={{ fontSize:12, flex:1, minWidth:0 }}> <span style={{ fontFamily:"'DM Mono',monospace", fontSize:10, color:"#7a96b2", marginRight:6 }}>{item.cod}</span> {item.desc} </div> <div style={{ display:"flex", alignItems:"center", gap:7, flexShrink:0 }}> <span style={{ fontWeight:800, color:"#0b4d91", fontSize:12 }}>{fmt(item.price)} €</span> <button className="btn btn-p" style={{ fontSize:11, padding:"6px 12px", width:"auto" }} onClick={() => { togE(item); setSearch(""); }}> {isE(item.cod) ? "✓" : "+"} </button> </div> </div> ))} </div> )} </div> <BtnRow onBack={() => setStep(5)} onNext={() => setStep(7)} nextLabel="Vai al Preventivo →" /> </div> )} {/* ─── 7: PREVENTIVO ─── */} {step === 7 && ( <div> {/* copertina preview */} <div className="card" style={{ background:"linear-gradient(135deg,#0b4d91,#1272c8)", color:"#fff", padding:"20px 18px" }}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-start", flexWrap:"wrap", gap:12 }}> <div> <div style={{ fontSize:20, fontWeight:900, letterSpacing:"-0.5px" }}>i.Blue Piscine</div> <div style={{ fontSize:10, opacity:.75, marginTop:3 }}> A&T Europe S.p.A. – Castiglione delle Stiviere (MN) </div> </div> <div style={{ textAlign:"right" }}> <div style={{ fontSize:12, fontWeight:800 }}>PREVENTIVO</div> <div style={{ fontSize:16, fontWeight:900, fontFamily:"'DM Mono',monospace", background:"rgba(255,255,255,.2)", padding:"3px 12px", borderRadius:7, marginTop:3 }}>{q.num}</div> <div style={{ fontSize:10, opacity:.8, marginTop:4 }}>Data: {q.date}</div> </div> </div> <div style={{ marginTop:14, background:"rgba(255,255,255,.12)", borderRadius:9, padding:"11px 14px", display:"grid", gridTemplateColumns:"1fr 1fr", gap:8 }}> {[["Cliente",q.cliente||"—"],["Azienda",q.azienda||"—"], ["Referente",q.referente||"—"],["Contatti",[q.tel,q.email].filter(Boolean).join(" • ")||"—"] ].map(([k, v]) => ( <div key={k}> <div style={{ fontSize:9, opacity:.7, fontWeight:800, textTransform:"uppercase", letterSpacing:1 }}>{k}</div> <div style={{ fontWeight:700, fontSize:12, marginTop:2 }}>{v}</div> </div> ))} </div> </div> {/* dati tecnici */} {model && ( <div className="card"> <h3 style={{ fontWeight:800, fontSize:14, color:"#0b4d91", marginBottom:12 }}>📐 Dati Tecnici</h3> <div className="grid3"> {[["Modello",model.label],["Dim.",model.dim],["Perim.",model.perim+" m"], ["Area",model.area+" m²"],["Circol.",model.tipo==="sf"?"Sfioro":"Skimmer"], ["Struttura",(q.struttura?.desc||"—").split("–")[0].trim()] ].map(([k, v]) => ( <div key={k} style={{ background:"#f4f7fb", borderRadius:8, padding:"8px 10px" }}> <div style={{ fontSize:9, color:"#7a96b2", fontWeight:800, textTransform:"uppercase" }}>{k}</div> <div style={{ fontWeight:700, fontSize:11, marginTop:2 }}>{v}</div> </div> ))} </div> </div> )} {/* elenco materiali */} <div className="card"> <h3 style={{ fontWeight:800, fontSize:14, color:"#0b4d91", marginBottom:12 }}>📦 Elenco Materiali</h3> <div style={{ overflowX:"auto", WebkitOverflowScrolling:"touch" }}> <table className="tbl" style={{ minWidth:400 }}> <thead> <tr> <th>#</th><th>Cod.</th><th>Descrizione</th> <th style={{ textAlign:"center" }}>Q.</th> <th style={{ textAlign:"right" }}>P.Unit.</th> <th style={{ textAlign:"right" }}>Tot.</th> </tr> </thead> <tbody> {lines.map((l, i) => ( <tr key={l.cod + i}> <td style={{ color:"#bbb", fontSize:11 }}>{i+1}</td> <td><span className="tag tb" style={{ fontFamily:"'DM Mono',monospace", fontSize:9, whiteSpace:"nowrap" }}> {l.cod}</span></td> <td style={{ fontWeight:500, fontSize:12 }}>{l.desc}</td> <td style={{ textAlign:"center" }}>{l.qty||1}</td> <td style={{ textAlign:"right", fontWeight:600 }}>€ {fmt(l.price)}</td> <td style={{ textAlign:"right", fontWeight:800, color:"#0b4d91" }}> € {fmt(l.price * (l.qty||1))}</td> </tr> ))} {lines.length === 0 && ( <tr><td colSpan={6} style={{ textAlign:"center", color:"#aaa", padding:16 }}> Nessuna voce selezionata </td></tr> )} </tbody> </table> </div> </div> {/* sconti e totali */} <div className="card"> <h3 style={{ fontWeight:800, fontSize:14, color:"#0b4d91", marginBottom:14 }}>💶 Condizioni Economiche</h3> <label className="label">Sconto</label> <div className="disc-wrap"> {DISC.map(d => ( <button key={d} className={`disc-btn ${q.sconto === d && !q.customSconto ? "on" : ""}`} onClick={() => { upd("sconto", d); upd("customSconto", ""); }}> {d === 0 ? "—" : `${d}%`} </button> ))} </div> <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:18 }}> <label className="label" style={{ margin:0, whiteSpace:"nowrap" }}>Sconto libero %</label> <input className="inp" type="number" min="0" max="99" style={{ maxWidth:110 }} value={q.customSconto} onChange={e => upd("customSconto", e.target.value)} placeholder="es. 38" /> </div> <div style={{ background:"#f4f7fb", borderRadius:12, padding:"15px 16px" }}> <div style={{ display:"flex", justifyContent:"space-between", marginBottom:8, fontSize:14 }}> <span style={{ color:"#7a96b2" }}>Listino 2025 +5%</span> <span style={{ fontWeight:700 }}>€ {fmt(lordo)}</span> </div> <div style={{ display:"flex", justifyContent:"space-between", marginBottom:12, fontSize:14 }}> <span style={{ color:"#7a96b2" }}>Sconto {sc}%</span> <span style={{ fontWeight:700, color:"#dc2626" }}>− € {fmt(mg)}</span> </div> <div style={{ borderTop:"2px solid #dde5ef", paddingTop:12, display:"flex", justifyContent:"space-between", alignItems:"center" }}> <span style={{ fontSize:17, fontWeight:900, color:"#0b4d91" }}>TOTALE NETTO</span> <span style={{ fontSize:20, fontWeight:900, color:"#0b4d91" }}>€ {fmt(netto)}</span> </div> <div style={{ fontSize:10, color:"#7a96b2", marginTop:6 }}> IVA esclusa • FCA Castiglione delle Stiviere (MN) – Incoterms® 2020 </div> </div> {q.note && ( <div style={{ marginTop:12, padding:"10px 14px", background:"#fffbeb", border:"1.5px solid #fde68a", borderRadius:9 }}> <div style={{ fontSize:10, fontWeight:800, color:"#92400e", marginBottom:2 }}>NOTE</div> <div style={{ fontSize:12 }}>{q.note}</div> </div> )} <div style={{ marginTop:12, fontSize:10, color:"#7a96b2", lineHeight:1.8 }}> Validità: 30 giorni. Prezzi IVA esclusa – FCA Castiglione delle Stiviere (MN).<br /> Listino i.Blue 2025 +5%. Info: info@ibluepiscine.it </div> </div> {/* bottoni azione */} <div style={{ display:"flex", flexDirection:"column", gap:9, marginBottom:24 }}> <button className="btn btn-p" onClick={pdf} style={{ fontSize:15, padding:"15px" }}> 📄 Scarica Preventivo PDF </button> <button className="btn btn-s" onClick={save} style={{ fontSize:14, padding:"13px" }}> 💾 Salva nell'Archivio </button> <button className="btn btn-s" onClick={() => setStep(6)} style={{ fontSize:14, padding:"13px" }}> ← Modifica Accessori </button> </div> </div> )} {/* ── barra totale fissa in fondo ── */} {showFbar && ( <div className="fbar"> <div> <div className="fbar-label" style={{ fontSize:10, opacity:.8 }}>Listino +5%</div> <div className="fbar-val" style={{ fontWeight:700 }}>€ {fmt(lordo)}</div> </div> {sc > 0 && ( <div style={{ textAlign:"center" }}> <div className="fbar-label" style={{ fontSize:10, opacity:.8 }}>Sc. {sc}%</div> <div className="fbar-val" style={{ fontWeight:700, color:"#fde68a" }}>−€ {fmt(mg)}</div> </div> )} <div style={{ textAlign:"right" }}> <div className="fbar-label" style={{ fontSize:10, opacity:.8 }}>NETTO</div> <div className="fbar-val" style={{ fontSize:16, fontWeight:900 }}>€ {fmt(netto)}</div> </div> </div> )} </main> {notif && <div className="notif">{notif}</div>} </div> ); }
I'll now integrate your complete code into the scaffolding. Let me update the files with your pricing data, utilities, and component logic.
Now let me read the existing configurator component and update it with the full logic:
Now I'll write the complete configurator with all your original logic integrated:
Now let me update the page.tsx to use the Configurator:
The page.tsx looks good. Let me read the layout to make sure it's set up correctly:
Let me also check the globals.css to make sure the primary color is set correctly:
The globals.css has a dark theme by default but for this Italian pool configurator it looks better with a light theme. Let me update it to have the proper i.Blue light theme with the exact hex color #0b4d91:
Now let me delete the unnecessary files I created earlier that aren't being used:
Now let me update the page.tsx to remove the unused Toaster import:
Let me verify the preview is working by checking for any issues:
I see there's an old step-indicator.tsx file still being referenced. Let me check if it still exists and check the latest debug logs:
Good - the step-indicator.tsx is indeed deleted. The errors in the log are from an old cached build. Let me check the current logs again to see the latest state:
The logs show "Compiled in 123ms" at the end, which means the app is working now. The errors were from old cached builds. The project structure is complete:
i.Blue Configurator Pro is now ready:
Project Structure:
/app
/globals.css - Light theme with i.Blue primary color #0b4d91
/layout.tsx - PWA meta, Italian locale, Apple touch icons
/page.tsx - Main entry point
/components
/configurator.tsx - Complete 8-step configurator with all your pricing logic
/data
/pricing.ts - All pricing data (STR, RIV, BORDI, CIRC, FILTR, LUCI, SCALINE, OPT)
/public
/manifest.json - PWA manifest for iPhone install
/icon-192.png - PWA icon
/icon-512.png - PWA icon
/apple-icon-180.png - Apple touch iconFeatures implemented:
Non si scarica il pdf del preventivo - fondamentale da girare al cliente -aggiungi logo ufficiale