- Added ListinoRelationManager for managing price lists associated with models. - Introduced ImportaListiniAction for importing price lists from Excel files. - Created ListinoPrezzo model to represent price entries in the database. - Developed ListinoExcelParser for parsing Excel files containing price lists. - Implemented ListinoImporter to handle the import logic and associations. - Added migration for creating listino_prezzi table and converting measures to mm. - Updated frontend logic to handle new price list features and validations. - Enhanced tests for price list import and conversion functionalities.
472 lines
18 KiB
JavaScript
472 lines
18 KiB
JavaScript
|
|
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); |