import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import $ from 'jquery'; import { APP } from './app.js'; import ParamsParser from './params_parser.js'; import MenuBuilder from './menu_builder.js'; import '@fortawesome/fontawesome-free/css/all.min.css'; import './price_calculator.js'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/js/bootstrap.bundle.min.js'; import Configurator from './configurator.js'; import AuthGate from './auth_gate.js'; import { API_BASE as PREVENTIVI_API_BASE } from './api_base.js'; window.$ = $; window.THREE = THREE; // Used by APP Scripts. window.OrbitControls = OrbitControls; // Used by APP Scripts. window.GLTFLoader = GLTFLoader; // Used by APP Scripts. // --- Integrazione backend preventivi --- const CART_TOKEN_KEY = 'kreios_cart_token'; // URL dell'app cliente (checkout): richiesto all'API invece di essere indovinato, perché la // porta Vite dell'app cliente può cambiare (es. se 5199 è già occupata). Fallback locale se // il backend non è raggiungibile. let appClienteUrl = (typeof window.APP_CLIENTE_URL === 'string' && window.APP_CLIENTE_URL) || 'http://localhost:5199'; const configReady = (async () => { try { const res = await fetch(`${PREVENTIVI_API_BASE}/config`, { headers: { Accept: 'application/json' }, }); if (!res.ok) return; const data = await res.json(); if (data.frontend_cliente_url) appClienteUrl = data.frontend_cliente_url; } catch (e) { // Backend non raggiungibile: resta il fallback locale. } })(); let invioPreventivoInCorso = false; // Mostra/aggiorna il link "Vai al preventivo" (markup fisso in index.html, sopra la cart-container). window.mostraLinkPreventivo = (token, count) => { const link = document.getElementById('vai-al-preventivo'); if (!link) return; // L'app cliente non conosce a priori l'URL del configuratore (può girare su domini/porte // diverse in dev/prod): lo passiamo qui come parametro, così il link "torna al configuratore" // nella pagina di conferma può essere generato dinamicamente invece di essere hardcoded. const ritorno = encodeURIComponent(window.location.href); let href = `${appClienteUrl}/?token=${encodeURIComponent(token)}&ritorno=${ritorno}`; // Se il cliente è loggato nel configuratore, passiamo il suo token anche al checkout // così può precompilare l'anagrafica già salvata (vedi AuthGate.getToken()). const clienteToken = AuthGate.getToken(); if (clienteToken) { href += `&cliente_token=${encodeURIComponent(clienteToken)}`; } link.href = href; link.textContent = count > 0 ? `Vai al preventivo (${count})` : 'Vai al preventivo'; link.classList.remove('d-none'); }; // Ripristina il link se esiste già un preventivo in corso da una sessione precedente. const ripristinaLinkPreventivo = async () => { const token = localStorage.getItem(CART_TOKEN_KEY); if (!token) return; try { await configReady; const res = await fetch(`${PREVENTIVI_API_BASE}/carrello/${encodeURIComponent(token)}`, { headers: { Accept: 'application/json' }, }); if (!res.ok) return; const data = await res.json(); if (data.carrello?.items?.length) { window.mostraLinkPreventivo(token, data.carrello.items.length); } } catch (e) { // Backend non raggiungibile: non blocca il configuratore. } }; ripristinaLinkPreventivo(); $(() => { $('.cart-button').on('click', async () => { if ($('.cart-button').hasClass('disabled') || invioPreventivoInCorso) return; invioPreventivoInCorso = true; try { const params = ParamsParser.getQueryParams(); const img = await window.generaImagine(); const conf = window.configurazione || {}; const inner = conf.configurazione || {}; const body = { token: localStorage.getItem(CART_TOKEN_KEY) || undefined, sistema: params.sistema, type: params.type, nome: inner.nome, larghezza: parseInt(inner.width) || null, altezza: parseInt(inner.height) || null, qty: parseInt($('.quantity-input').val()) || 1, prezzo: conf.prezzo, payload: inner, img: img, }; await configReady; const res = await fetch(`${PREVENTIVI_API_BASE}/carrello/aggiungi`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error('Errore nel salvataggio del preventivo'); const data = await res.json(); localStorage.setItem(CART_TOKEN_KEY, data.token); window.mostraLinkPreventivo(data.token, data.carrello.items.length); } catch (e) { console.error(e); alert('Si è verificato un errore nell\'aggiunta al preventivo. Riprova.'); } finally { invioPreventivoInCorso = false; } }); }); const start = async () => { const params = ParamsParser.getQueryParams(); // Sistemi e modelli (con icone) sono serviti dal backoffice, già filtrati sui soli attivi // e nell'ordine del menu: il primo sistema è quello di default. const res = await fetch(`${PREVENTIVI_API_BASE}/sistemi`, { headers: { Accept: 'application/json' } }); if (!res.ok) throw new Error(`Catalogo non disponibile (HTTP ${res.status})`); const sistemi = await res.json(); if (!sistemi.length) throw new Error('Nessun sistema attivo nel catalogo'); const sistema = sistemi.find(s => s.codice === params.sistema) || sistemi[0]; params.sistema = sistema.codice; let data_folder = `./assets/data/sistemi/${params.sistema}`; let modello_folder = `./assets/data/modelli/${params.sistema}`; const modelli = sistema.modelli; if (!modelli.length) throw new Error(`Nessun modello attivo per il sistema ${sistema.codice}`); let modello = modelli.find(m => m.id === params.type); // Senza type (o type non valido) si mostra il primo infisso disponibile if (!modello) { modello = modelli.find(m => m.enabled !== false) || modelli[0]; params.type = modello.id; } // Allinea la query string ai valori effettivi, così le altre letture (es. carrello) restano coerenti const url = new URL(window.location.href); if (url.searchParams.get('sistema') !== params.sistema || url.searchParams.get('type') !== params.type) { url.searchParams.set('sistema', params.sistema); url.searchParams.set('type', params.type); window.history.replaceState(null, '', url); } const data = await (await fetch(`${modello_folder}/${modello.cartella}/form.json?rnd=${Math.random()}`)).json(); const data_finiture = modello.moduli.includes('modello') ? (await (await fetch(`${modello_folder}/${modello.cartella}/modello.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; const data_maniglie = modello.moduli.includes('maniglia') ? (await (await fetch(`${modello_folder}/${modello.cartella}/maniglia.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; const data_avvolgibile = modello.moduli.includes('avvolgibile') ? (await (await fetch(`${modello_folder}/${modello.cartella}/avvolgibile.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; const data_vetri = modello.moduli.includes('vetro') ? (await (await fetch(`${modello_folder}/${modello.cartella}/vetro.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; const listino = modello.moduli.includes('listino') ? await (await fetch(`${PREVENTIVI_API_BASE}/sistemi/${encodeURIComponent(params.sistema)}/modelli/${encodeURIComponent(modello.id)}/listino`, { headers: { Accept: 'application/json' } })).json() : []; if (modello.moduli.includes('listino') && !listino.length) throw new Error(`Nessun listino caricato per il modello ${modello.id}`); const altezza_maniglia = modello.moduli.includes('altezza_maniglia') ? await (await fetch(`${data_folder}/altezza_maniglia_${modello.categoria}.json?rnd=${Math.random()}`)).json() : []; let priceCalculator = await MenuBuilder.build(data, 'left-container', modello.cartella_texture || modello.cartella, { modelli: modelli, categoria: modello.categoria, sistema: params.sistema, data_finiture: data_finiture, data_maniglie: data_maniglie, data_avvolgibile: data_avvolgibile, data_vetri: data_vetri, listino: listino, modello_id: modello.id }); Configurator.init(priceCalculator, { modello: modello, nome_esteso: data.nome, data_finiture: data_finiture, data_maniglie: data_maniglie, data_avvolgibile: data_avvolgibile, data_vetri: data_vetri, altezza_maniglia: altezza_maniglia, listino: listino }); Configurator.enable(); $(`.menu-header-sistema .${params.sistema}`).addClass('selected'); $(`.v-divider.${params.sistema}`).addClass('d-none'); var player = new APP.Player('canvas-container'); var playerOff = new APP.Player('canvas-offscreen'); var loader = new THREE.FileLoader(); loader.load('./assets/app.json', function (text) { player.load(JSON.parse(text)); playerOff.load(JSON.parse(text)); player.rotateBackground(Math.PI); playerOff.rotateBackground(Math.PI); playerOff.setSize(playerOff.dom.clientWidth, playerOff.dom.clientHeight); var gltfLoader = new GLTFLoader(); gltfLoader.load(`./assets/models/${params.sistema}/${modello.modello}`, function (gltf) { document.getElementById('loadingBarContainer').style.display = 'none'; //gltf.scene.rotation.y = THREE.MathUtils.degToRad(80); gltf.scene.name = 'infisso'; window.current_model = modello.modello; gltf.scene.userData.name = modello.modello; //$("input[type=radio]").first().trigger("click"); gltf.scene.position.y = 0; gltf.scene.position.z = 0.1; //gltf.scene.rotation.set(0, Math.PI, 0); player.addToScene(gltf.scene); player.viewFrontSide(); playerOff.addToScene(gltf.scene.clone()); playerOff.viewFrontSide(); $('#canvas-offscreen').addClass('d-none'); player.dom.style.position = 'relative'; if (params.type != 'cassonetto') { $("input[name=maniglia]").first().trigger("click"); $("input[name=finitura_interna]").eq(1).trigger("click"); $("input[name=finitura_interna]").eq(0).trigger("click"); } $("#larghezza").first().trigger("change"); //player.addMarker("Cylinder028", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); //player.addMarker("marker_1", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); //player.addMarker("marker_2", 'right', 'https://www.youtube.com/embed/LCi9jdF_4MQ?si=b8cyxSOI2sKEmmcm'); }, function (xhr) { var percentComplete = (xhr.loaded / xhr.total) * 100; document.getElementById('loadingBarProgress').style.width = percentComplete + '%'; document.getElementById('loadingPercentage').innerText = Math.round(percentComplete) + '%'; }, function (error) { console.error('An error happened while loading the GLB file:', error); }); player.setSize(player.dom.clientWidth, player.dom.clientHeight); player.play(); window.addEventListener('resize', function () { //console.log(player.dom.clientWidth, player.dom.clientHeight); player.setSize(player.dom.clientWidth, player.dom.clientHeight); player.render(); }); }); $('.quantity-btn').on('click', (e) => { const input = document.querySelector('.quantity-input'); let value = parseInt(input.value); if (e.target.classList.contains('increment')) { input.value = value + 1; } else if (e.target.classList.contains('decrement') && value > 1) { input.value = value - 1; } }); window.generaImagine = async () => { playerOff.viewFrontSide(0.6); playerOff.render(); $('#canvas-offscreen').removeClass('d-none'); const canvas = $('#canvas-offscreen canvas')[0]; const dataUrl = await canvas.toDataURL('image/png'); $('#canvas-offscreen').addClass('d-none'); playerOff.viewFrontSide(); return dataUrl; const a = document.createElement('a'); a.href = dataUrl; a.download = 'immagine.png'; a.click(); } window.render = () => { player.render(); } let currentView = 'front'; window.cambiaTexture = (data) => { return new Promise(async (resolve, reject) => { try { await player.changeTexture(data) //console.log(data) //console.log('window.rotate', window.rotate); if (data && data.categories.includes('esterno') && currentView !== 'back'){ if(window.rotate){ player.flipView(); currentView = 'back'; } } if (data && (data.categories.includes('interno') || data.categories.includes('maniglia')) && currentView !== 'front'){ if(window.rotate){ player.flipView(); currentView = 'front'; } } render(); resolve(); } catch (error) { reject(error); } }); } window.flip180 = () => { player.flipView(); currentView = currentView === 'front' ? 'back' : 'front'; render(); } window.checkFlip = (direction) => { if(direction != currentView) flip180(); } window.cambiaColoreCB = (data) => { player.changeColor(data); render(); } window.cambiaApertura = (name, scale) => { player.changeXscale(name, scale); render(); } window.mostraCerniere = (categoria) => { player.showObject(categoria); render(); } window.nascondiCerniere = (categoria) => { player.hideObject(categoria); render(); } window.rotate = true; window.caricaModello = (modello) => { let textureButtons = $(".texture-button:checked"); window.rotate = false; const radios = {}; $('.texture-button[name]').each(function() { radios[$(this).attr('name')] ??= this; }); document.querySelectorAll(".texture-button[name=maniglia]").forEach(el => el.checked = false) //console.log(textureButtons) if(player.swapModel(modello)){ for(let el in radios){ $(radios[el]).trigger("click"); } $(textureButtons).each((i, el) => { $(el).trigger("click"); }); setTimeout(() => { window.rotate = true; }, 1000); // return; } document.getElementById('loadingBarContainerAlt').style.display = 'flex'; document.getElementById('loadingBarProgressAlt').style.width = '0%'; document.getElementById('loadingPercentageAlt').innerText = '0%'; var gltfLoader = new GLTFLoader(); gltfLoader.load(`./assets/models/${modello}`, function (gltf) { document.getElementById('loadingBarContainerAlt').style.display = 'none'; $('#canvas-offscreen').removeClass('d-none'); //gltf.scene.rotation.y = THREE.MathUtils.degToRad(80); gltf.scene.name = 'infisso'; gltf.scene.userData.name = modello; setTimeout(() => { for(let el in radios){ $(radios[el]).trigger("click"); } }); gltf.scene.position.y = 0; gltf.scene.position.z = 0.1; //gltf.scene.rotation.set(0, Math.PI, 0); player.hideModel('infisso'); playerOff.hideModel('infisso'); player.addToScene(gltf.scene); player.viewFrontSide(); playerOff.addToScene(gltf.scene.clone()); //playerOff.viewFrontSide(); $('#canvas-offscreen').addClass('d-none'); player.dom.style.position = 'relative'; if (params.type != 'cassonetto') { $("input[name=maniglia]").first().trigger("click"); } $("#larghezza").first().trigger("change"); $(textureButtons).each((i, el) => { $(el).trigger("click"); }); setTimeout(() => { window.rotate = true; }, 1000); //player.addMarker("Cylinder028", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); //player.addMarker("marker_1", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); //player.addMarker("marker_2", 'right', 'https://www.youtube.com/embed/LCi9jdF_4MQ?si=b8cyxSOI2sKEmmcm'); }, function (xhr) { var percentComplete = (xhr.loaded / xhr.total) * 100; document.getElementById('loadingBarProgressAlt').style.width = percentComplete + '%'; document.getElementById('loadingPercentageAlt').innerText = Math.round(percentComplete) + '%'; }, function (error) { console.error('An error happened while loading the GLB file:', error); }); } } AuthGate.ensure().then(start);