Files
kreios/src/js/starter.js
T
carmine.savino 1e352eb2ef feat: Implement price list management and import functionality
- 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.
2026-10-19 16:51:24 +02:00

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);