diff --git a/.gitignore b/.gitignore index f1bee86..e25a23a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ /backoffice/cliente/dist/ /node_modules/ kreios.zip -.env.production \ No newline at end of file +.env.production +Thumbs.db +/reference/icone_profili_extra \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3f0c6ee..6bce9f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,16 +41,21 @@ Entry point is `src/js/starter.js`, loaded from `src/index.html`. Flow on load ( *Note: that doc describes a more modular refactor (`Application.js`, `communication/UrlParamsParser.js`, `communication/IframeBridge.js`) that does not exist yet in `src/js` — the current implementation is the flatter `starter.js`/`params_parser.js` described here.* -2. Product/catalog data is fetched as JSON from `src/assets/data/sistemi//modelli.json` and - `src/assets/data/modelli///{form,modello,maniglia,avvolgibile,vetro,listino}.json` - (which JSON files are fetched depends on the `moduli` array in the matched `modelli.json` entry). +2. Catalog data comes from the Laravel backoffice (`backoffice/`, API base in `src/js/api_base.js`): + `GET /api/sistemi` (systems + active models that have a price list; models without one are hidden and flagged in the admin list) and + `GET /api/sistemi//modelli//configurazione`, which returns the menu wiring (`form`: radios, + selects, finish menus — the old `form.json` shape), finishes, handles, glass types and the price-grid sizes, + with image URLs served by `GET /api/media/...` (CORS-enabled, needed for WebGL textures). Only + `avvolgibile.json` and `sistemi//altezza_maniglia_.json` are still read from `src/assets/data`. 3. **`MenuBuilder.build()`** (`src/js/menu_builder.js`) injects an HTML template (`assets/_menu.html`, or `assets/_menu-cassonetto.html` when `type=cassonetto`) into `#left-container`, builds the model-picker list, handles the mobile-portrait/landscape layout reflow, and initializes `PriceCalculator`. It returns the price calculator instance used by the rest of the app. 4. **`Configurator.init()` / `.enable()`** (`src/js/configurator.js`) wires a single delegated `input` handler - on the size fields, selects and radios. On every change it recomputes price via `PriceCalculator`, builds - `window.configurazione` (the object that gets posted to the parent on "add to cart"), and updates the DOM. + on the size fields, selects and radios. On every change (debounced) it sends the selected codes to + `POST /api/sistemi//modelli//prezzo`, stores the result in `window.configurazione` and + updates the DOM. "Add to cart" sends the same codes (`scelte`): the backend recomputes the price and ignores + any price coming from the client. 5. **`APP.Player`** (`src/js/app.js`) wraps a Three.js renderer/scene/camera (scene graph loaded from `src/assets/app.json`, a three.js-editor-style export) plus the actual product GLB (`src/assets/models//`). Two players are created: `player` (visible canvas) and @@ -59,22 +64,30 @@ Entry point is `src/js/starter.js`, loaded from `src/index.html`. Flow on load ( per `userData.categoria` mesh tag — categories like `interno`, `esterno`, `maniglia`, `vetro`, `cerniere`), `flipView` (180° GSAP-animated turn between front/back view), `addMarker` (in-scene hotspot with a video popup). -6. **`PriceCalculator`** (`src/js/price_calculator.js`) is a stateless-ish module (single module-level - `listini` cache set via `init`) that looks up a base price from a width/height grid in **mm** (served by - the backoffice API, using the first grid size ≥ the requested one) and layers on percentage/flat supplements for finish, glass, hinges, handle - height, and actuator type, then adds 22% VAT. +6. **Pricing is server-side only**: `backoffice/app/Support/Configuratore/CalcolatorePrezzo.php`. + `prezzo = base × (1 + supplementi % / 100) + supplementi fissi + variazione vetro` (VAT excluded). Base is + the first grid size ≥ the requested one (mm). Supplements come from `opzioni` rows (finish type + bianco/uniforme/bicolore, hinges, profili extra, ...): percentage, fixed, or per linear metre + (ml from L/H multipliers + extra mm). Groups with `ui = multipla` (profili extra) accept several codes, + with same-`alternativa` options mutually exclusive. Sistema-level rows can be overridden per model by group. + Option groups themselves live in `gruppi_opzioni` (title, control radio/select/multipla, menu section, and + the model `moduli` key that enables them): a group without fixed markup in `_menu.html` is rendered by + `MenuBuilder` into `.opzioni-sistema` / `.opzioni-accessori`. Select options can call a `window[func]` + (e.g. `nascondiCategoria` with `categoria: maniglia` for the motor) via their `parametri`. The glass + variation is Σ max(pane area, minimo_mq) × maggiorazione_mq, with panes from the model's `regole_vetro` + (spec: "Specifica tecnica vetri"). All of it is edited in Filament under "Configuratore". + `src/js/price_calculator.js` only wraps the API call and the grid sizes. ### Data conventions - Every 3D-visible part is tagged with `userData.categoria` in the GLB/scene so JS can show/hide/retexture it without knowing mesh names (`interno`, `esterno`, `maniglia`, `cerniere`, `vetro`, ...). -- Each model folder under `src/assets/data/modelli///` follows the same file set: - `form.json` (UI radios/selects/finiture wiring — declares which named ` +
- ${option.descrizione} + ${option.descrizione}
@@ -334,8 +323,6 @@ const buildTextureRadio = async (model, parent, icon_folder, texture_folder, cat const isStale = () => textureRequestVersion[parent] !== requestVersion; option.categories = categories; - option.folder = model; - option.texture_folder = texture_folder; option.isStale = isStale; $(".mobile-popup-backdrop, .mobile-popup").addClass("d-none"); @@ -357,15 +344,43 @@ const buildTextureRadio = async (model, parent, icon_folder, texture_folder, cat //container.querySelector(`.${parent}`).innerHTML = html; }; +const assicuraContenitore = (key, gruppo) => { + if (container.querySelector(`.${key}`)) return; + + const controllo = gruppo.ui === 'select' + ? `` + : ``; + + $(container).find(`.opzioni-${gruppo.sezione}`).append(`
+
+ +
+
`); + + if (gruppo.sezione === 'sistema') $('.sistema-menu').removeClass('d-none'); +}; + +const buildMultiple = (options, key) => options.map(option => `
+ +
${option.label}
+
`).join(''); + const buildRadios = (options, key, model) => { let html = ''; let has_sub_options = false; options.forEach((option, i) => { - let percentuale = ''; let modello = ''; - let class_percentuale = ''; let has_options = false; let sub_option_container = null; let html_func = ''; @@ -391,11 +406,6 @@ const buildRadios = (options, key, model) => { modello = `data-model="${option.model}"`; } - if (option.percentuale != null) { - percentuale = `data-percentuale="${option.percentuale}"`; - class_percentuale = 'percentuale'; - } - if (has_options) { has_options_class = 'has-options'; html_func = `onclick="toggleSubOption('${sub_option_container.id}',event)"`; @@ -407,9 +417,9 @@ const buildRadios = (options, key, model) => { html += `