Refactor PVC models and update tests

- Updated the PVC models in the database and adjusted the tests accordingly to reflect the new model counts and codes.
- Removed deprecated models from the JSON data for PVC systems.
- Added new GLB models for "scorrevole_sf_3_ante" and "scorrevole_sf_4_ante".
- Enhanced the development scripts to streamline the build process for both frontends.
- Updated documentation to reflect changes in the development environment and deployment process.
- Improved the styling of texture radio options for better visibility.
- Fixed URL handling in the configurator to ensure proper routing and API calls.
This commit is contained in:
2026-10-20 18:40:59 +02:00
parent b6a32edfe6
commit 0dfda3a338
33 changed files with 339 additions and 366 deletions
+2
View File
@@ -15,6 +15,8 @@
/auth.json
node_modules
/public/build
/public/configuratore
/public/preventivo
/public/fonts-manifest.dev.json
/public/hot
/public/storage
+12 -24
View File
@@ -18,7 +18,7 @@ riassuntivo e invia le email a cliente e amministratore.
2. `starter.js` invia `POST /api/carrello/aggiungi` (con `sistema`, `type`, dimensioni, prezzo, payload,
screenshot PNG). Il backend crea/aggiorna un `preventivo` (stato `bozza`) identificato da un `token`
salvato in `localStorage`.
3. Il pulsante "Vai al preventivo" apre l'app cliente passando il token: `…/?token=XXX`.
3. Il pulsante "Vai al preventivo" apre l'app cliente passando il token: `/preventivo/?token=XXX`.
4. L'app cliente mostra il carrello, l'utente compila i dati e conferma → `POST /api/carrello/{token}/conferma`.
5. Il backend crea il `cliente`, imposta lo stato a `inviato`, genera il PDF e invia le email
(cliente con PDF allegato + notifica admin). Il preventivo è visibile nel backoffice.
@@ -33,20 +33,14 @@ php artisan migrate --seed # crea le tabelle + utente admin
php artisan storage:link
php artisan serve --port=8000 # API + admin su http://localhost:8000
# 2. App cliente (cartella cliente/)
cd cliente
pnpm install
pnpm dev # http://localhost:5199
# 3. Configuratore (root del repo)
# 2. Frontend: configuratore + app cliente, compilati in public/ (dalla root del repo)
cd ..
pnpm dev # http://localhost:5173
./build.sh # -> public/configuratore e public/preventivo
```
Le tre porte (8000 / 5199 / 5173) sono fisse (`strictPort: true` nei rispettivi `vite.config.js`): se una è
già occupata l'avvio fallisce con un errore esplicito invece di spostarsi silenziosamente su un'altra porta
— evita che il configuratore e l'app cliente finiscano per caso sulla stessa porta (successo in passato,
dato che entrambi avevano `5174` come default).
Configuratore e app cliente sono serviti da questo backend: <http://localhost:8000/configuratore/?sistema=pvc&type=finestra_2_ante>
e <http://localhost:8000/preventivo/>. In sviluppo `./dev.sh` (root) avvia Laravel e ricompila i frontend
a ogni modifica. Dettagli in `../docs/PREVENTIVI_BACKOFFICE.md`.
### Credenziali admin (seed di default)
- URL: <http://localhost:8000/admin>
@@ -62,19 +56,13 @@ In produzione configurare SMTP nel `.env`.
|---|---|
| `DB_*` | MySQL: `127.0.0.1`, `root`, no password, `kreios_preventivi` |
| `ADMIN_NOTIFY_EMAIL` | Email che riceve la notifica di nuovo preventivo |
| `APP_FRONTEND_CLIENTE_URL` | URL dell'app cliente (per i link) |
| `CORS_ALLOWED_ORIGINS` | Origini autorizzate all'API (default `*` in dev) |
| `APP_FRONTEND_CLIENTE_URL` | URL dell'app cliente, solo se non servita da qui (default `APP_URL/preventivo`) |
| `CORS_ALLOWED_ORIGINS` | Origini esterne autorizzate all'API (default `*`; i frontend serviti da qui non ne hanno bisogno) |
### Configuratore (`../src/js/starter.js`)
`PREVENTIVI_API_BASE` è l'unico indirizzo che il configuratore deve conoscere a priori (default
`http://localhost:8000/api`, sovrascrivibile con `window.PREVENTIVI_API_BASE` prima del bundle). L'URL
dell'app cliente **non è hardcoded**: all'avvio il configuratore chiama `GET /api/config` e usa il
`frontend_cliente_url` restituito da Laravel (letto da `APP_FRONTEND_CLIENTE_URL` nel `.env`) — così non c'è
da tenere sincronizzati due URL se la porta Vite dell'app cliente cambia (es. per conflitto con altre porte
occupate). `window.APP_CLIENTE_URL` resta come fallback locale se il backend non è raggiungibile.
### App cliente (`cliente/.env`)
`VITE_API_BASE` — URL base dell'API Laravel (default `http://localhost:8000/api`).
### Frontend
Configuratore e app cliente chiamano l'API sulla stessa origine (`${location.origin}/api`): nessuna
variabile da impostare. `VITE_PREVENTIVI_API_BASE` (root) / `VITE_API_BASE` (`cliente/`) servono solo per
un'API su un altro dominio. Il link "Vai al preventivo" usa `frontend_cliente_url` da `GET /api/config`.
## Modello dati
-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/jpeg" href="/kreios-logo.jpg" />
<title>Il tuo preventivo · Krèios</title>
<script type="module" crossorigin src="/assets/index-CmMOSSq4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C_ir1rDB.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+5 -2
View File
@@ -2,6 +2,9 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { api, resolveToken, resolveReturnUrl, resolveClienteToken, clearToken, formatEuro } from './api.js'
// Servita sotto /preventivo/: i file di public/ vanno risolti rispetto alla base della build.
const logoUrl = `${import.meta.env.BASE_URL}kreios-logo.jpg`
const step = ref('cart') // cart | checkout | done
const loading = ref(true)
const submitting = ref(false)
@@ -158,7 +161,7 @@ onMounted(loadCarrello)
<template>
<header class="app-header">
<img src="/kreios-logo.jpg" alt="Krèios" />
<img :src="logoUrl" alt="Krèios" />
<h1>Il tuo preventivo</h1>
</header>
@@ -340,7 +343,7 @@ onMounted(loadCarrello)
<!-- STEP CONFERMA -->
<template v-else-if="step === 'done'">
<div class="card center">
<img class="done-logo" src="/kreios-logo.jpg" alt="Krèios" />
<img class="done-logo" :src="logoUrl" alt="Krèios" />
<div class="success-icon">✅</div>
<h2>Grazie!</h2>
<p>La tua richiesta di preventivo <b>n. {{ numeroConfermato }}</b> è stata inviata.</p>
+2 -1
View File
@@ -1,4 +1,5 @@
const BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000/api'
// L'app cliente è servita dal backend stesso (/preventivo/): l'API è sulla stessa origine.
const BASE = import.meta.env.VITE_API_BASE || `${window.location.origin}/api`
async function request(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
+6 -4
View File
@@ -3,9 +3,11 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
host: true,
port: 5199,
strictPort: true,
// L'app cliente è servita da Laravel sotto /preventivo/ (stessa origine dell'API):
// la build finisce direttamente in backoffice/public/preventivo.
base: '/preventivo/',
build: {
outDir: '../public/preventivo',
emptyOutDir: true,
},
})
+6 -4
View File
@@ -4,11 +4,13 @@ return [
// Email a cui inviare la notifica di nuovo preventivo.
'admin_email' => env('ADMIN_NOTIFY_EMAIL'),
// URL base dell'app cliente (per link nelle email admin).
'frontend_cliente_url' => env('APP_FRONTEND_CLIENTE_URL', 'http://localhost:5199'),
// URL base dell'app cliente (per link nelle email admin). Di default è servita da questo
// backend sotto /preventivo (build in public/preventivo, vedi routes/web.php).
'frontend_cliente_url' => env('APP_FRONTEND_CLIENTE_URL', rtrim((string) env('APP_URL', 'http://localhost'), '/').'/preventivo'),
// URL del configuratore 3D (linkato dal menu del backoffice).
'configuratore_url' => env('APP_CONFIGURATORE_URL', 'http://localhost:5173'),
// URL del configuratore 3D (linkato dal menu del backoffice). Di default è servito da questo
// backend sotto /configuratore/ (build in public/configuratore).
'configuratore_url' => env('APP_CONFIGURATORE_URL', rtrim((string) env('APP_URL', 'http://localhost'), '/').'/configuratore/'),
// Regola base di scontistica (in %), valida per tutti i clienti salvo personalizzazione:
// imponibile = totale parziale x (1 - 1° sconto) x (1 - 2° sconto extra).
@@ -0,0 +1,86 @@
<?php
use App\Models\Modello;
use App\Models\Sistema;
use Illuminate\Database\Migrations\Migration;
/**
* Scorrevoli SF PVC: i modelli 3 e 4 ante hanno ora il file 3D e vanno nel
* configuratore (listino, regole vetro e opzioni ci sono già); 1 anta SF e parallelo
* 1 anta escono dalla gamma (non hanno listino né opzioni proprie).
*/
return new class extends Migration
{
private const NUOVI = [
'scorrevole_sf_3_ante' => 'SCORREVOLE 3 ANTE SF PVC - SALAMANDER',
'scorrevole_sf_4_ante' => 'SCORREVOLE 4 ANTE SF PVC - SALAMANDER',
];
private const ELIMINATI = ['scorrevole_sf_1_anta', 'scorrevole_parallelo_1_anta'];
/** Moduli che il 2 ante ha in più rispetto ai modelli senza configurazione 3D. */
private const MODULI_3D = ['listino', 'maniglia', 'modello', 'vetro'];
public function up(): void
{
$pvc = Sistema::where('codice', 'pvc')->first();
if (! $pvc) {
return;
}
foreach (self::NUOVI as $codice => $titolo) {
$modello = $pvc->modelli()->where('codice', $codice)->first();
$modello?->update([
'attivo' => true,
'cartella' => $codice,
'file_modello' => "{$codice}.glb",
'cartella_texture' => 'finestra_2_ante',
'titolo' => $modello->titolo ?: $titolo,
'moduli' => array_values(array_unique([...self::MODULI_3D, ...($modello->moduli ?? [])])),
]);
}
// Una alla volta: l'evento deleted rimuove anche l'icona dal disco.
$pvc->modelli()->whereIn('codice', self::ELIMINATI)->get()->each->delete();
$this->dopoIl2Ante($pvc);
}
/** Attivandosi finirebbero in fondo agli attivi: li mette subito dopo lo scorrevole SF 2 ante. */
private function dopoIl2Ante(Sistema $pvc): void
{
$codici = $pvc->modelli()->orderBy('ordine')->orderBy('id')->pluck('codice')
->diff(array_keys(self::NUOVI))->values()->all();
$posizione = array_search('scorrevole_sf_2_ante', $codici, true);
if ($posizione !== false) {
array_splice($codici, $posizione + 1, 0, array_keys(self::NUOVI));
}
foreach ($codici as $i => $codice) {
$pvc->modelli()->where('codice', $codice)->update(['ordine' => $i + 1]);
}
Modello::normalizzaOrdine($pvc->id);
}
public function down(): void
{
// I modelli eliminati non vengono ricreati: si recuperano con il CatalogoSeeder
// da una versione precedente di database/seeders/data/modelli_pvc.php.
$pvc = Sistema::where('codice', 'pvc')->first();
if (! $pvc) {
return;
}
foreach (array_keys(self::NUOVI) as $codice) {
$modello = $pvc->modelli()->where('codice', $codice)->first();
$modello?->update([
'attivo' => false,
'cartella' => null,
'file_modello' => null,
'cartella_texture' => null,
'moduli' => array_values(array_diff($modello->moduli ?? [], self::MODULI_3D)),
]);
}
}
};
@@ -107,8 +107,6 @@ return [
'scorrevole_hst_2_ante' => ['ALZ2', ['finitura_esterna', 'apertura']],
'scorrevole_hst_4_ante' => ['ALZ4', ['finitura_esterna', 'apertura']],
// Non presenti nella specifica vetri: nessuna variazione di prezzo per il vetro.
'scorrevole_sf_1_anta' => [null, ['finitura_esterna', 'apertura']],
'scorrevole_parallelo_1_anta' => [null, ['finitura_esterna', 'apertura']],
'scorrevole_sf_2_ante' => ['SC2', ['finitura_esterna', 'apertura']],
'scorrevole_sf_3_ante' => ['SC3', ['finitura_esterna', 'apertura']],
'scorrevole_sf_4_ante' => ['SC4', ['finitura_esterna', 'apertura']],
@@ -636,9 +636,9 @@ return [
'portafinestra_3_ante' => 'PORTAFINESTRA 3 ANTE PVC - SALAMANDER BE 82',
'scorrevole_hst_1_anta' => 'SCORREVOLE 1 ANTA HTS PVC - SALAMANDER',
'scorrevole_hst_2_ante' => 'SCORREVOLE 2 ANTE HTS PVC - SALAMANDER',
'scorrevole_parallelo_1_anta' => 'SCORREVOLE PARALLELO 1 ANTA PVC - SALAMANDER',
'scorrevole_sf_1_anta' => 'SCORREVOLE 1 ANTA SF PVC - SALAMANDER',
'scorrevole_sf_2_ante' => 'SCORREVOLE 2 ANTE SF PVC - SALAMANDER',
'scorrevole_sf_3_ante' => 'SCORREVOLE 3 ANTE SF PVC - SALAMANDER',
'scorrevole_sf_4_ante' => 'SCORREVOLE 4 ANTE SF PVC - SALAMANDER',
'scuretto_base' => 'SCURETTO BASE PVC - SALAMANDER',
'scuretto_pannello_alessandrite' => 'SCURETTO ALESSANDRITE PVC - SALAMANDER',
'scuretto_pannello_malachite' => 'SCURETTO MALACHITE PVC - SALAMANDER',
@@ -171,17 +171,6 @@ return [
'cartella_texture' => null,
'moduli' => null,
],
[
'codice' => 'scorrevole_sf_1_anta',
'nome' => 'Scorrevole SF 1 Anta',
'descrizione' => null,
'attivo' => true,
'cartella' => 'scorrevole_sf_1_anta',
'file_modello' => 'scorrevole_sf_1_anta.glb',
'categoria' => 'scorrevoli',
'cartella_texture' => 'finestra_2_ante',
'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'],
],
[
'codice' => 'scorrevole_sf_2_ante',
'nome' => 'Scorrevole SF 2 Ante',
@@ -197,23 +186,23 @@ return [
'codice' => 'scorrevole_sf_3_ante',
'nome' => 'Scorrevole SF 3 Ante',
'descrizione' => 'SCORREVOLE IN LINEA A 3 ANTE',
'attivo' => false,
'cartella' => null,
'file_modello' => null,
'attivo' => true,
'cartella' => 'scorrevole_sf_3_ante',
'file_modello' => 'scorrevole_sf_3_ante.glb',
'categoria' => 'scorrevoli',
'cartella_texture' => null,
'moduli' => null,
'cartella_texture' => 'finestra_2_ante',
'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'],
],
[
'codice' => 'scorrevole_sf_4_ante',
'nome' => 'Scorrevole SF 4 Ante',
'descrizione' => 'SCORREVOLE IN LINEA A 4 ANTE',
'attivo' => false,
'cartella' => null,
'file_modello' => null,
'attivo' => true,
'cartella' => 'scorrevole_sf_4_ante',
'file_modello' => 'scorrevole_sf_4_ante.glb',
'categoria' => 'scorrevoli',
'cartella_texture' => null,
'moduli' => null,
'cartella_texture' => 'finestra_2_ante',
'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'],
],
[
'codice' => 'traslante_scorrevole_1_anta',
@@ -237,17 +226,6 @@ return [
'cartella_texture' => null,
'moduli' => null,
],
[
'codice' => 'scorrevole_parallelo_1_anta',
'nome' => 'Scorrevole Parallelo 1 Anta',
'descrizione' => null,
'attivo' => true,
'cartella' => 'scorrevole_parallelo_1_anta',
'file_modello' => 'scorrevole_parallelo_1_anta.glb',
'categoria' => 'scorrevoli',
'cartella_texture' => 'finestra_2_ante',
'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'],
],
[
'codice' => 'bilico',
'nome' => 'Bilico Orizzontale',
+19
View File
@@ -2,6 +2,7 @@
use App\Models\Preventivo;
use App\Support\PreventivoPdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
@@ -20,3 +21,21 @@ Route::get('/admin/preventivi/{preventivo}/pdf', function (Preventivo $preventiv
'preventivo-'.($preventivo->numero ?? $preventivo->id).'.pdf'
);
})->middleware(['auth'])->name('preventivi.pdf.show');
// Configuratore 3D e app cliente (preventivo) sono build Vite statiche dentro public/ (vedi build.sh):
// i file (js, css, assets) li serve direttamente il web server, qui gestiamo solo l'index.
// Lo slash finale è obbligatorio perché i path runtime sono relativi (./assets/...).
foreach (['configuratore', 'preventivo'] as $frontend) {
Route::get($frontend, function (Request $request) use ($frontend) {
if (! str_ends_with($request->getPathInfo(), '/')) {
$query = $request->getQueryString();
return redirect('/'.$frontend.'/'.($query ? '?'.$query : ''), 301);
}
$index = public_path($frontend.'/index.html');
abort_unless(is_file($index), 404, "Build di {$frontend} assente: eseguire build.sh.");
return response()->file($index, ['Cache-Control' => 'no-cache']);
})->name($frontend);
}
@@ -62,12 +62,9 @@ class CatalogoAdminTest extends TestCase
{
$codici = fn () => Modello::orderBy('ordine')->pluck('codice')->all();
// Ordine di OrdineSerramenti per gli attivi, poi i 4 disattivi nello stesso ordine.
// Ordine di OrdineSerramenti per gli attivi, poi i 2 disattivi nello stesso ordine.
$this->assertSame(['finestra_1_anta', 'finestra_2_ante', 'finestra_3_ante'], array_slice($codici(), 0, 3));
$this->assertSame(
['scorrevole_hst_4_ante', 'scorrevole_sf_3_ante', 'scorrevole_sf_4_ante', 'traslante_scorrevole_1_anta_f'],
array_slice($codici(), -4)
);
$this->assertSame(['scorrevole_hst_4_ante', 'traslante_scorrevole_1_anta_f'], array_slice($codici(), -2));
// Disattivando un modello passa nel gruppo in fondo (dopo i 25 attivi rimasti),
// mantenendo l'ordine relativo con gli altri disattivi; riattivandolo resta
@@ -81,7 +78,7 @@ class CatalogoAdminTest extends TestCase
$this->assertSame(['celino', 'bilico', 'scorrevole_hst_4_ante'], array_slice($codici(), 24, 3));
// In tabella i disattivi stanno in fondo anche se l'ordine salvato li metterebbe in mezzo.
Modello::where('codice', 'scorrevole_sf_3_ante')->update(['ordine' => 2]);
Modello::where('codice', 'scorrevole_hst_4_ante')->update(['ordine' => 2]);
Livewire::test(ListModelli::class)
->set('tableRecordsPerPage', 50)
->assertCanSeeTableRecords(
+4 -4
View File
@@ -26,14 +26,14 @@ class CatalogoApiTest extends TestCase
$this->seed(CatalogoSeeder::class);
$pvc = Sistema::where('codice', 'pvc')->firstOrFail();
$this->assertSame(30, $pvc->modelli()->count());
$this->assertSame(28, $pvc->modelli()->count());
$finestra = $pvc->modelli()->where('codice', 'finestra_1_anta')->firstOrFail();
$this->assertSame('FINESTRA 1 ANTA, A/R', $finestra->descrizione);
Storage::disk('public')->assertExists($finestra->icona);
// Modelli presenti solo nell'Excel: senza asset 3D, quindi disattivi.
$this->assertFalse($pvc->modelli()->where('codice', 'scorrevole_sf_3_ante')->value('attivo'));
$this->assertFalse($pvc->modelli()->where('codice', 'scorrevole_hst_4_ante')->value('attivo'));
}
public function test_il_seeder_non_sovrascrive_le_modifiche_admin(): void
@@ -44,7 +44,7 @@ class CatalogoApiTest extends TestCase
$this->seed(CatalogoSeeder::class);
$this->assertSame('Bilico modificato', Modello::where('codice', 'bilico')->value('nome'));
$this->assertSame(30, Modello::count());
$this->assertSame(28, Modello::count());
}
public function test_api_sistemi_espone_solo_modelli_attivi_e_con_listino_in_ordine(): void
@@ -63,7 +63,7 @@ class CatalogoApiTest extends TestCase
$this->assertSame(25, $modelli->count());
$this->assertNotContains('bilico', $modelli->pluck('id'));
$this->assertSame('finestra_1_anta', $modelli->first()['id']);
$this->assertNotContains('scorrevole_sf_3_ante', $modelli->pluck('id'));
$this->assertNotContains('scorrevole_hst_4_ante', $modelli->pluck('id'));
$this->assertStringEndsWith('/storage/modelli/icone/pvc/finestra_1_anta.jpg', $modelli->first()['icona']);
$this->assertSame('finestra_1_anta.glb', $modelli->first()['modello']);
}
@@ -230,6 +230,6 @@ class ImportazioneListiniTest extends TestCase
]);
// Modello disattivo (senza asset 3D): non esposto.
$this->getJson('/api/sistemi/pvc/modelli/scorrevole_sf_3_ante/listino')->assertNotFound();
$this->getJson('/api/sistemi/pvc/modelli/scorrevole_hst_4_ante/listino')->assertNotFound();
}
}