diff --git a/Ordine dei serramenti + nome.xlsx b/Ordine dei serramenti + nome.xlsx deleted file mode 100644 index 4457efc..0000000 Binary files a/Ordine dei serramenti + nome.xlsx and /dev/null differ diff --git a/backoffice/app/Filament/Resources/ModelloResource.php b/backoffice/app/Filament/Resources/ModelloResource.php new file mode 100644 index 0000000..b1e404c --- /dev/null +++ b/backoffice/app/Filament/Resources/ModelloResource.php @@ -0,0 +1,157 @@ +schema([ + Forms\Components\Section::make('Anagrafica') + ->columns(2) + ->schema([ + Forms\Components\Select::make('sistema_id') + ->label('Sistema') + ->relationship('sistema', 'nome') + ->required() + ->preload() + ->live(), + Forms\Components\TextInput::make('codice') + ->label('Codice interno') + ->helperText('Usato dal configuratore nel parametro ?type=.') + ->required() + ->maxLength(100) + ->alphaDash() + ->unique( + ignoreRecord: true, + modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('sistema_id', $get('sistema_id')), + ), + Forms\Components\TextInput::make('nome') + ->label('Nome') + ->helperText('Etichetta mostrata nella lista modelli del configuratore.') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('descrizione') + ->label('Descrizione') + ->maxLength(255), + Forms\Components\FileUpload::make('icona') + ->label('Icona') + ->image() + ->disk('public') + ->directory('modelli/icone') + ->maxSize(2048), + Forms\Components\Grid::make(1) + ->columnSpan(1) + ->schema([ + Forms\Components\TextInput::make('ordine') + ->label('Ordine') + ->numeric() + ->minValue(0) + ->default(0), + Forms\Components\Toggle::make('attivo') + ->label('Visibile nel configuratore') + ->helperText('Richiede la configurazione 3D.') + ->default(true) + ->live(), + ]), + ]), + Forms\Components\Section::make('Configurazione 3D') + ->description('Asset del configuratore usati per questo modello.') + ->columns(2) + ->collapsible() + ->schema([ + Forms\Components\TextInput::make('cartella') + ->label('Cartella dati') + ->helperText('src/assets/data/modelli///') + ->required(fn (Get $get) => (bool) $get('attivo')), + Forms\Components\TextInput::make('file_modello') + ->label('File modello 3D') + ->helperText('src/assets/models//.glb') + ->required(fn (Get $get) => (bool) $get('attivo')), + Forms\Components\Select::make('categoria') + ->label('Categoria') + ->options(Modello::CATEGORIE) + ->required(fn (Get $get) => (bool) $get('attivo')), + Forms\Components\TextInput::make('cartella_texture') + ->label('Cartella texture') + ->helperText('Se vuota si usa la cartella dati.'), + Forms\Components\CheckboxList::make('moduli') + ->label('Moduli') + ->options(Modello::MODULI) + ->columns(3) + ->columnSpanFull(), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->defaultSort('ordine') + ->reorderable('ordine') + ->defaultGroup('sistema.nome') + ->columns([ + Tables\Columns\ImageColumn::make('icona')->label('Icona')->disk('public')->height(48), + Tables\Columns\TextColumn::make('codice')->label('Codice')->searchable()->copyable(), + Tables\Columns\TextColumn::make('nome')->label('Nome')->searchable(), + Tables\Columns\TextColumn::make('descrizione')->label('Descrizione')->searchable()->wrap()->placeholder('—'), + Tables\Columns\TextColumn::make('categoria') + ->label('Categoria') + ->badge() + ->formatStateUsing(fn (?string $state) => Modello::CATEGORIE[$state] ?? $state) + ->placeholder('—'), + Tables\Columns\ToggleColumn::make('attivo') + ->label('Attivo') + ->disabled(fn (Modello $record) => ! $record->attivo && (! $record->cartella || ! $record->file_modello)), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('sistema')->label('Sistema')->relationship('sistema', 'nome'), + Tables\Filters\SelectFilter::make('categoria')->label('Categoria')->options(Modello::CATEGORIE), + Tables\Filters\TernaryFilter::make('attivo')->label('Attivo'), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListModelli::route('/'), + 'create' => Pages\CreateModello::route('/create'), + 'edit' => Pages\EditModello::route('/{record}/edit'), + ]; + } +} diff --git a/backoffice/app/Filament/Resources/ModelloResource/Pages/CreateModello.php b/backoffice/app/Filament/Resources/ModelloResource/Pages/CreateModello.php new file mode 100644 index 0000000..b55444a --- /dev/null +++ b/backoffice/app/Filament/Resources/ModelloResource/Pages/CreateModello.php @@ -0,0 +1,16 @@ +getResource()::getUrl('index'); + } +} diff --git a/backoffice/app/Filament/Resources/ModelloResource/Pages/EditModello.php b/backoffice/app/Filament/Resources/ModelloResource/Pages/EditModello.php new file mode 100644 index 0000000..47ebbf2 --- /dev/null +++ b/backoffice/app/Filament/Resources/ModelloResource/Pages/EditModello.php @@ -0,0 +1,24 @@ +getResource()::getUrl('index'); + } +} diff --git a/backoffice/app/Filament/Resources/ModelloResource/Pages/ListModelli.php b/backoffice/app/Filament/Resources/ModelloResource/Pages/ListModelli.php new file mode 100644 index 0000000..15f6e9e --- /dev/null +++ b/backoffice/app/Filament/Resources/ModelloResource/Pages/ListModelli.php @@ -0,0 +1,19 @@ +schema([ + Forms\Components\Section::make() + ->columns(2) + ->schema([ + Forms\Components\TextInput::make('codice') + ->label('Codice') + ->helperText('Usato dal configuratore nel parametro ?sistema= (es. pvc).') + ->required() + ->maxLength(50) + ->alphaDash() + ->unique(ignoreRecord: true), + Forms\Components\TextInput::make('nome') + ->label('Nome') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('ordine') + ->label('Ordine') + ->numeric() + ->minValue(0) + ->default(0), + Forms\Components\Toggle::make('attivo') + ->label('Attivo nel configuratore') + ->default(true) + ->inline(false), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->defaultSort('ordine') + ->reorderable('ordine') + ->columns([ + Tables\Columns\TextColumn::make('codice')->label('Codice')->searchable(), + Tables\Columns\TextColumn::make('nome')->label('Nome')->searchable(), + Tables\Columns\TextColumn::make('modelli_count')->label('Modelli')->counts('modelli'), + Tables\Columns\IconColumn::make('attivo')->label('Attivo')->boolean(), + ]) + ->actions([ + Tables\Actions\Action::make('modelli') + ->label('Modelli') + ->icon('heroicon-o-squares-2x2') + ->color('gray') + ->url(fn (Sistema $record) => ModelloResource::getUrl('index', [ + 'tableFilters' => ['sistema' => ['value' => $record->getKey()]], + ])), + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make() + ->modalDescription('Verrà eliminato definitivamente il sistema insieme a tutti i suoi modelli.'), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListSistemi::route('/'), + 'create' => Pages\CreateSistema::route('/create'), + 'edit' => Pages\EditSistema::route('/{record}/edit'), + ]; + } +} diff --git a/backoffice/app/Filament/Resources/SistemaResource/Pages/CreateSistema.php b/backoffice/app/Filament/Resources/SistemaResource/Pages/CreateSistema.php new file mode 100644 index 0000000..9cdaab7 --- /dev/null +++ b/backoffice/app/Filament/Resources/SistemaResource/Pages/CreateSistema.php @@ -0,0 +1,16 @@ +getResource()::getUrl('index'); + } +} diff --git a/backoffice/app/Filament/Resources/SistemaResource/Pages/EditSistema.php b/backoffice/app/Filament/Resources/SistemaResource/Pages/EditSistema.php new file mode 100644 index 0000000..95a08cd --- /dev/null +++ b/backoffice/app/Filament/Resources/SistemaResource/Pages/EditSistema.php @@ -0,0 +1,25 @@ +modalDescription('Verrà eliminato definitivamente il sistema insieme a tutti i suoi modelli.'), + ]; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } +} diff --git a/backoffice/app/Filament/Resources/SistemaResource/Pages/ListSistemi.php b/backoffice/app/Filament/Resources/SistemaResource/Pages/ListSistemi.php new file mode 100644 index 0000000..8eb968a --- /dev/null +++ b/backoffice/app/Filament/Resources/SistemaResource/Pages/ListSistemi.php @@ -0,0 +1,19 @@ +with(['modelli' => fn ($q) => $q->attivi()]) + ->get(); + + return response()->json($sistemi->map(fn (Sistema $s) => $this->sistema($s))->values()); + } + + /** + * GET /api/sistemi/{codice} + */ + public function sistemaPerCodice(string $codice): JsonResponse + { + $sistema = Sistema::attivi() + ->where('codice', $codice) + ->with(['modelli' => fn ($q) => $q->attivi()]) + ->firstOrFail(); + + return response()->json($this->sistema($sistema)); + } + + private function sistema(Sistema $sistema): array + { + return [ + 'codice' => $sistema->codice, + 'nome' => $sistema->nome, + 'modelli' => $sistema->modelli->map(fn (Modello $m) => $this->modello($m))->values(), + ]; + } + + // Stessa forma del vecchio modelli.json (id, nome, cartella, modello, ...) più descrizione e icona. + private function modello(Modello $modello): array + { + return [ + 'id' => $modello->codice, + 'nome' => $modello->nome, + 'descrizione' => $modello->descrizione, + 'icona' => $modello->icona_url, + 'cartella' => $modello->cartella, + 'modello' => $modello->file_modello, + 'categoria' => $modello->categoria, + 'cartella_texture' => $modello->cartella_texture, + 'moduli' => $modello->moduli ?? [], + 'enabled' => true, + ]; + } +} diff --git a/backoffice/app/Models/Modello.php b/backoffice/app/Models/Modello.php new file mode 100644 index 0000000..940e1f4 --- /dev/null +++ b/backoffice/app/Models/Modello.php @@ -0,0 +1,78 @@ +.json). + public const CATEGORIE = [ + 'finestre' => 'Finestre', + 'porte' => 'Porte', + 'scorrevoli' => 'Scorrevoli', + 'scuretti' => 'Scuretti', + 'accessori' => 'Accessori', + ]; + + // Moduli dati caricati dal configuratore per il modello. + public const MODULI = [ + 'listino' => 'Listino', + 'modello' => 'Finiture', + 'maniglia' => 'Maniglia', + 'altezza_maniglia' => 'Altezza maniglia', + 'vetro' => 'Vetro', + 'cerniere' => 'Cerniere', + 'telaio' => 'Telaio', + 'avvolgibile' => 'Avvolgibile', + 'select_finiture' => 'Select finiture', + ]; + + protected $fillable = [ + 'sistema_id', 'codice', 'nome', 'descrizione', 'icona', 'ordine', 'attivo', + 'cartella', 'file_modello', 'categoria', 'cartella_texture', 'moduli', + ]; + + protected $casts = [ + 'ordine' => 'integer', + 'attivo' => 'boolean', + 'moduli' => 'array', + ]; + + public function sistema(): BelongsTo + { + return $this->belongsTo(Sistema::class); + } + + public function scopeAttivi(Builder $query): Builder + { + return $query->where('attivo', true)->orderBy('ordine')->orderBy('id'); + } + + public function getIconaUrlAttribute(): ?string + { + return $this->icona ? Storage::disk('public')->url($this->icona) : null; + } + + protected static function booted(): void + { + // Rimuove il file dell'icona sostituita o del modello eliminato. + static::updated(function (Modello $modello): void { + $vecchia = $modello->getOriginal('icona'); + if ($modello->wasChanged('icona') && $vecchia) { + Storage::disk('public')->delete($vecchia); + } + }); + + static::deleted(function (Modello $modello): void { + if ($modello->icona) { + Storage::disk('public')->delete($modello->icona); + } + }); + } +} diff --git a/backoffice/app/Models/Sistema.php b/backoffice/app/Models/Sistema.php new file mode 100644 index 0000000..ec34adf --- /dev/null +++ b/backoffice/app/Models/Sistema.php @@ -0,0 +1,31 @@ + 'integer', + 'attivo' => 'boolean', + ]; + + public function modelli(): HasMany + { + return $this->hasMany(Modello::class); + } + + public function scopeAttivi(Builder $query): Builder + { + return $query->where('attivo', true)->orderBy('ordine')->orderBy('id'); + } +} diff --git a/backoffice/database/migrations/2026_10_19_100000_create_sistemi_table.php b/backoffice/database/migrations/2026_10_19_100000_create_sistemi_table.php new file mode 100644 index 0000000..ce33c00 --- /dev/null +++ b/backoffice/database/migrations/2026_10_19_100000_create_sistemi_table.php @@ -0,0 +1,26 @@ +id(); + // Codice usato dal configuratore nel parametro ?sistema= (es. "pvc"). + $table->string('codice', 50)->unique(); + $table->string('nome'); + $table->unsignedInteger('ordine')->default(0); + $table->boolean('attivo')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sistemi'); + } +}; diff --git a/backoffice/database/migrations/2026_10_19_100001_create_modelli_table.php b/backoffice/database/migrations/2026_10_19_100001_create_modelli_table.php new file mode 100644 index 0000000..0ee33b3 --- /dev/null +++ b/backoffice/database/migrations/2026_10_19_100001_create_modelli_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('sistema_id')->constrained('sistemi')->cascadeOnDelete(); + // Codice interno: è anche il ?type= del configuratore. + $table->string('codice', 100); + // Etichetta mostrata nella lista modelli del configuratore. + $table->string('nome'); + // Descrizione d'ordine (es. "FINESTRA 1 ANTA, A/R"). + $table->string('descrizione')->nullable(); + $table->string('icona')->nullable(); + $table->unsignedInteger('ordine')->default(0); + $table->boolean('attivo')->default(true); + + // Configurazione 3D letta dal configuratore. + $table->string('cartella')->nullable(); + $table->string('file_modello')->nullable(); + $table->string('categoria', 50)->nullable(); + $table->string('cartella_texture')->nullable(); + $table->json('moduli')->nullable(); + + $table->timestamps(); + + $table->unique(['sistema_id', 'codice']); + $table->index(['sistema_id', 'attivo', 'ordine']); + }); + } + + public function down(): void + { + Schema::dropIfExists('modelli'); + } +}; diff --git a/backoffice/database/seeders/CatalogoSeeder.php b/backoffice/database/seeders/CatalogoSeeder.php new file mode 100644 index 0000000..fcd88c4 --- /dev/null +++ b/backoffice/database/seeders/CatalogoSeeder.php @@ -0,0 +1,50 @@ + 'pvc'], ['nome' => 'PVC', 'ordine' => 1]); + + $this->seedModelli($pvc, require __DIR__.'/data/modelli_pvc.php'); + } + + private function seedModelli(Sistema $sistema, array $modelli): void + { + foreach ($modelli as $ordine => $dati) { + $modello = Modello::firstOrCreate( + ['sistema_id' => $sistema->id, 'codice' => $dati['codice']], + $dati + ['ordine' => $ordine + 1] + ); + + if (! $modello->icona) { + $modello->icona = $this->copiaIcona($sistema->codice, $modello->codice); + $modello->save(); + } + } + } + + private function copiaIcona(string $sistema, string $codice): ?string + { + $sorgente = __DIR__."/data/icone/{$sistema}/{$codice}.jpg"; + if (! is_file($sorgente)) { + return null; + } + + $destinazione = "modelli/icone/{$sistema}/{$codice}.jpg"; + Storage::disk('public')->put($destinazione, file_get_contents($sorgente)); + + return $destinazione; + } +} diff --git a/backoffice/database/seeders/DatabaseSeeder.php b/backoffice/database/seeders/DatabaseSeeder.php index 9bfaa9b..2455252 100644 --- a/backoffice/database/seeders/DatabaseSeeder.php +++ b/backoffice/database/seeders/DatabaseSeeder.php @@ -21,5 +21,7 @@ class DatabaseSeeder extends Seeder 'password' => Hash::make('password'), ] ); + + $this->call(CatalogoSeeder::class); } } diff --git a/backoffice/database/seeders/data/icone/pvc/bilico.jpg b/backoffice/database/seeders/data/icone/pvc/bilico.jpg new file mode 100644 index 0000000..8c1f098 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/bilico.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/cassonetto_139.jpg b/backoffice/database/seeders/data/icone/pvc/cassonetto_139.jpg new file mode 100644 index 0000000..9e0bcac Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/cassonetto_139.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/cassonetto_150.jpg b/backoffice/database/seeders/data/icone/pvc/cassonetto_150.jpg new file mode 100644 index 0000000..9e0bcac Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/cassonetto_150.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/cassonetto_80.jpg b/backoffice/database/seeders/data/icone/pvc/cassonetto_80.jpg new file mode 100644 index 0000000..9e0bcac Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/cassonetto_80.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/celino.jpg b/backoffice/database/seeders/data/icone/pvc/celino.jpg new file mode 100644 index 0000000..2b82a17 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/celino.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/finestra_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/finestra_1_anta.jpg new file mode 100644 index 0000000..21ee92c Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/finestra_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/finestra_2_ante.jpg b/backoffice/database/seeders/data/icone/pvc/finestra_2_ante.jpg new file mode 100644 index 0000000..a431b93 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/finestra_2_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/finestra_3_ante.jpg b/backoffice/database/seeders/data/icone/pvc/finestra_3_ante.jpg new file mode 100644 index 0000000..fd4886d Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/finestra_3_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/finestra_anta_fissa.jpg b/backoffice/database/seeders/data/icone/pvc/finestra_anta_fissa.jpg new file mode 100644 index 0000000..98dc4cf Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/finestra_anta_fissa.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/finestra_vasistas.jpg b/backoffice/database/seeders/data/icone/pvc/finestra_vasistas.jpg new file mode 100644 index 0000000..f1181a6 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/finestra_vasistas.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/porta_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/porta_1_anta.jpg new file mode 100644 index 0000000..3877777 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/porta_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/porta_1_anta_antipanico.jpg b/backoffice/database/seeders/data/icone/pvc/porta_1_anta_antipanico.jpg new file mode 100644 index 0000000..387495a Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/porta_1_anta_antipanico.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/porta_2_ante.jpg b/backoffice/database/seeders/data/icone/pvc/porta_2_ante.jpg new file mode 100644 index 0000000..409ddef Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/porta_2_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/porta_2_ante_antipanico.jpg b/backoffice/database/seeders/data/icone/pvc/porta_2_ante_antipanico.jpg new file mode 100644 index 0000000..2b068d7 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/porta_2_ante_antipanico.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/portafinestra_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/portafinestra_1_anta.jpg new file mode 100644 index 0000000..fd67bd5 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/portafinestra_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/portafinestra_2_ante.jpg b/backoffice/database/seeders/data/icone/pvc/portafinestra_2_ante.jpg new file mode 100644 index 0000000..4868b5c Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/portafinestra_2_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/portafinestra_3_ante.jpg b/backoffice/database/seeders/data/icone/pvc/portafinestra_3_ante.jpg new file mode 100644 index 0000000..9adeab1 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/portafinestra_3_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scorrevole_hst_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/scorrevole_hst_1_anta.jpg new file mode 100644 index 0000000..7040771 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scorrevole_hst_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scorrevole_hst_2_ante.jpg b/backoffice/database/seeders/data/icone/pvc/scorrevole_hst_2_ante.jpg new file mode 100644 index 0000000..a5d9aa4 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scorrevole_hst_2_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scorrevole_parallelo_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/scorrevole_parallelo_1_anta.jpg new file mode 100644 index 0000000..d2f53d0 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scorrevole_parallelo_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scorrevole_sf_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/scorrevole_sf_1_anta.jpg new file mode 100644 index 0000000..df6b85f Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scorrevole_sf_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scorrevole_sf_2_ante.jpg b/backoffice/database/seeders/data/icone/pvc/scorrevole_sf_2_ante.jpg new file mode 100644 index 0000000..4f1edf8 Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scorrevole_sf_2_ante.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scuretto_base.jpg b/backoffice/database/seeders/data/icone/pvc/scuretto_base.jpg new file mode 100644 index 0000000..19d244c Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scuretto_base.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scuretto_pannello_alessandrite.jpg b/backoffice/database/seeders/data/icone/pvc/scuretto_pannello_alessandrite.jpg new file mode 100644 index 0000000..19d244c Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scuretto_pannello_alessandrite.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/scuretto_pannello_malachite.jpg b/backoffice/database/seeders/data/icone/pvc/scuretto_pannello_malachite.jpg new file mode 100644 index 0000000..19d244c Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/scuretto_pannello_malachite.jpg differ diff --git a/backoffice/database/seeders/data/icone/pvc/traslante_scorrevole_1_anta.jpg b/backoffice/database/seeders/data/icone/pvc/traslante_scorrevole_1_anta.jpg new file mode 100644 index 0000000..78d4b9d Binary files /dev/null and b/backoffice/database/seeders/data/icone/pvc/traslante_scorrevole_1_anta.jpg differ diff --git a/backoffice/database/seeders/data/modelli_pvc.php b/backoffice/database/seeders/data/modelli_pvc.php new file mode 100644 index 0000000..2091087 --- /dev/null +++ b/backoffice/database/seeders/data/modelli_pvc.php @@ -0,0 +1,339 @@ + 'finestra_1_anta', + 'nome' => 'Finestra 1 anta', + 'descrizione' => 'FINESTRA 1 ANTA, A/R', + 'attivo' => true, + 'cartella' => 'finestra_1_anta', + 'file_modello' => 'finestra_1_anta.glb', + 'categoria' => 'finestre', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'finestra_2_ante', + 'nome' => 'Finestra 2 ante', + 'descrizione' => 'FINESTRA 2 ANTE, A/R', + 'attivo' => true, + 'cartella' => 'finestra_2_ante', + 'file_modello' => 'finestra_2_ante.glb', + 'categoria' => 'finestre', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'finestra_3_ante', + 'nome' => 'Finestra 3 ante', + 'descrizione' => 'FINESTRA 3 ANTE, A/R', + 'attivo' => true, + 'cartella' => 'finestra_3_ante', + 'file_modello' => 'finestra_3_ante.glb', + 'categoria' => 'finestre', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'finestra_anta_fissa', + 'nome' => 'Finestra Anta Fissa', + 'descrizione' => 'TELAIO FISSO', + 'attivo' => true, + 'cartella' => 'finestra_anta_fissa', + 'file_modello' => 'finestra_anta_fissa.glb', + 'categoria' => 'finestre', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'telaio', 'listino', 'modello', 'vetro', 'select_finiture'], + ], + [ + 'codice' => 'finestra_vasistas', + 'nome' => 'Finestra Vasistas', + 'descrizione' => 'WASISTAS CON MARTELLINA', + 'attivo' => true, + 'cartella' => 'finestra_vasistas', + 'file_modello' => 'finestra_vasistas.glb', + 'categoria' => 'finestre', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'portafinestra_1_anta', + 'nome' => 'Portafinestra 1 anta', + 'descrizione' => 'PORTAFINESTRA 1 ANTA, A/R', + 'attivo' => true, + 'cartella' => 'portafinestra_1_anta', + 'file_modello' => 'portafinestra_1_anta.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'portafinestra_2_ante', + 'nome' => 'Portafinestra 2 ante', + 'descrizione' => 'PORTAFINESTRA 2 ANTE, A/R', + 'attivo' => true, + 'cartella' => 'portafinestra_2_ante', + 'file_modello' => 'portafinestra_2_ante.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'portafinestra_3_ante', + 'nome' => 'Portafinestra 3 ante', + 'descrizione' => 'PORTAFINESTRA 3 ANTE, A/R', + 'attivo' => true, + 'cartella' => 'portafinestra_3_ante', + 'file_modello' => 'portafinestra_3_ante.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'porta_1_anta', + 'nome' => 'Porta 1 Anta', + 'descrizione' => 'PORTA 1 ANTA, CON SERRATURA , SOGLIA RIBASSATA', + 'attivo' => true, + 'cartella' => 'porta_1_anta', + 'file_modello' => 'porta_1_anta.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'porta_1_anta_antipanico', + 'nome' => 'Porta 1 Anta Antipanico', + 'descrizione' => 'PORTA 1 ANTA ANTIPANICO_AP ESTERNA, SOGLIA', + 'attivo' => true, + 'cartella' => 'porta_1_anta_antipanico', + 'file_modello' => 'porta_1_anta_antipanico.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'porta_2_ante', + 'nome' => 'Porta 2 Ante', + 'descrizione' => 'PORTA 2 ANTE, CON SERRATURA , SOGLIA', + 'attivo' => true, + 'cartella' => 'porta_2_ante', + 'file_modello' => 'porta_2_ante.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'porta_2_ante_antipanico', + 'nome' => 'Porta 2 Ante Antipanico', + 'descrizione' => 'PORTA 2 ANTE ANTIPANICO_AP ESTERNA, SOGLIA', + 'attivo' => true, + 'cartella' => 'porta_2_ante_antipanico', + 'file_modello' => 'porta_2_ante_antipanico.glb', + 'categoria' => 'porte', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'scorrevole_hst_1_anta', + 'nome' => 'Scorrevole HST 1 Anta', + 'descrizione' => 'ALZANTE SCORREVOLE 1 ANTA SCORREVOLE_+1 ANTA FISSA', + 'attivo' => true, + 'cartella' => 'scorrevole_hst_1_anta', + 'file_modello' => 'scorrevole_hst_1_anta.glb', + 'categoria' => 'scorrevoli', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'scorrevole_hst_2_ante', + 'nome' => 'Scorrevole HST 2 Ante', + 'descrizione' => 'ALZANTE SCORREVOLE 2 ANTE', + 'attivo' => true, + 'cartella' => 'scorrevole_hst_2_ante', + 'file_modello' => 'scorrevole_hst_2_ante.glb', + 'categoria' => 'scorrevoli', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'scorrevole_hst_4_ante', + 'nome' => 'Scorrevole HST 4 Ante', + 'descrizione' => 'ALZANTE SCORREVOLE 4 ANTE', + 'attivo' => false, + 'cartella' => null, + 'file_modello' => null, + 'categoria' => 'scorrevoli', + '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', + 'descrizione' => 'SCORREVOLE IN LINEA A 2 ANTE', + 'attivo' => true, + 'cartella' => 'scorrevole_sf_2_ante', + 'file_modello' => 'scorrevole_sf_2_ante.glb', + 'categoria' => 'scorrevoli', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'scorrevole_sf_3_ante', + 'nome' => 'Scorrevole SF 3 Ante', + 'descrizione' => 'SCORREVOLE IN LINEA A 3 ANTE', + 'attivo' => false, + 'cartella' => null, + 'file_modello' => null, + 'categoria' => 'scorrevoli', + 'cartella_texture' => null, + 'moduli' => null, + ], + [ + 'codice' => 'scorrevole_sf_4_ante', + 'nome' => 'Scorrevole SF 4 Ante', + 'descrizione' => 'SCORREVOLE IN LINEA A 4 ANTE', + 'attivo' => false, + 'cartella' => null, + 'file_modello' => null, + 'categoria' => 'scorrevoli', + 'cartella_texture' => null, + 'moduli' => null, + ], + [ + 'codice' => 'traslante_scorrevole_1_anta', + 'nome' => 'Traslante Scorrevole 1 Anta', + 'descrizione' => 'TRASLANTE SCORREVOLE, 1 ANTA SC.+1ANTA FISSA', + 'attivo' => true, + 'cartella' => 'traslante_scorrevole_1_anta', + 'file_modello' => 'traslante_scorrevole_1_anta.glb', + 'categoria' => 'scorrevoli', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'traslante_scorrevole_1_anta_f', + 'nome' => 'Traslante Scorrevole 1 Anta + Fisso Laterale', + 'descrizione' => 'TRASLANTE SCORREVOLE, 1 ANTA SC.+1 FISSO LATERALE', + 'attivo' => false, + 'cartella' => null, + 'file_modello' => null, + 'categoria' => 'scorrevoli', + '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', + 'descrizione' => 'BILICO ORIZZONTALE', + 'attivo' => true, + 'cartella' => 'bilico', + 'file_modello' => 'bilico.glb', + 'categoria' => 'finestre', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['cerniere', 'listino', 'maniglia', 'altezza_maniglia', 'modello', 'vetro'], + ], + [ + 'codice' => 'scuretto_base', + 'nome' => 'Scuretto Pannello Base', + 'descrizione' => 'SCURETTO BASE', + 'attivo' => true, + 'cartella' => 'scuretto_base', + 'file_modello' => 'scuretto_base.glb', + 'categoria' => 'scuretti', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], + [ + 'codice' => 'scuretto_pannello_alessandrite', + 'nome' => 'Scuretto Pannello Alessandrite', + 'descrizione' => 'SCURETTO ALESSANDRITE', + 'attivo' => true, + 'cartella' => 'scuretto_pannello_alessandrite', + 'file_modello' => 'scuretto_pannello_alessandrite.glb', + 'categoria' => 'scuretti', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], + [ + 'codice' => 'scuretto_pannello_malachite', + 'nome' => 'Scuretto Pannello Malachite', + 'descrizione' => 'SCURETTO MALACHITE', + 'attivo' => true, + 'cartella' => 'scuretto_pannello_malachite', + 'file_modello' => 'scuretto_pannello_malachite.glb', + 'categoria' => 'scuretti', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], + [ + 'codice' => 'cassonetto_80', + 'nome' => 'Cassonetto Monoblocco 80', + 'descrizione' => 'CASSONETTO 80mm', + 'attivo' => true, + 'cartella' => 'cassonetto_80', + 'file_modello' => 'cassonetto_80.glb', + 'categoria' => 'accessori', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], + [ + 'codice' => 'cassonetto_139', + 'nome' => 'Cassonetto Monoblocco 139', + 'descrizione' => 'CASSONETTO 139mm', + 'attivo' => true, + 'cartella' => 'cassonetto_139', + 'file_modello' => 'cassonetto_139.glb', + 'categoria' => 'accessori', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], + [ + 'codice' => 'cassonetto_150', + 'nome' => 'Cassonetto Monoblocco 150', + 'descrizione' => 'CASSONETTO 150mm', + 'attivo' => true, + 'cartella' => 'cassonetto_150', + 'file_modello' => 'cassonetto_150.glb', + 'categoria' => 'accessori', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], + [ + 'codice' => 'celino', + 'nome' => 'Celino', + 'descrizione' => 'CIELINO', + 'attivo' => true, + 'cartella' => 'celino', + 'file_modello' => 'celino.glb', + 'categoria' => 'accessori', + 'cartella_texture' => 'finestra_2_ante', + 'moduli' => ['modello', 'listino'], + ], +]; diff --git a/backoffice/routes/api.php b/backoffice/routes/api.php index 5a8ed9c..e0f08f1 100644 --- a/backoffice/routes/api.php +++ b/backoffice/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\CarrelloController; +use App\Http\Controllers\Api\CatalogoController; use Illuminate\Support\Facades\Route; // Espone al configuratore l'URL corrente dell'app cliente (checkout), così il @@ -10,6 +11,10 @@ Route::get('config', fn () => response()->json([ 'frontend_cliente_url' => config('preventivi.frontend_cliente_url'), ])); +// Catalogo prodotti (sistemi e modelli con icone) per il configuratore. +Route::get('sistemi', [CatalogoController::class, 'sistemi']); +Route::get('sistemi/{codice}', [CatalogoController::class, 'sistemaPerCodice']); + Route::prefix('auth')->group(function () { Route::post('login', [AuthController::class, 'login']); Route::middleware('auth:sanctum')->get('me', [AuthController::class, 'me']); diff --git a/backoffice/tests/Feature/CatalogoAdminTest.php b/backoffice/tests/Feature/CatalogoAdminTest.php new file mode 100644 index 0000000..a5e95d5 --- /dev/null +++ b/backoffice/tests/Feature/CatalogoAdminTest.php @@ -0,0 +1,75 @@ +seed(CatalogoSeeder::class); + $this->actingAs(User::factory()->create()); + } + + public function test_le_pagine_admin_del_catalogo_si_aprono(): void + { + $modello = Modello::where('codice', 'finestra_1_anta')->firstOrFail(); + + Livewire::test(ListSistemi::class)->assertOk()->assertSee('PVC'); + Livewire::test(ListModelli::class)->assertOk()->assertCanSeeTableRecords([$modello]); + Livewire::test(CreateModello::class)->assertOk(); + Livewire::test(EditModello::class, ['record' => $modello->getRouteKey()]) + ->assertOk() + ->assertFormSet(['codice' => 'finestra_1_anta', 'descrizione' => 'FINESTRA 1 ANTA, A/R']); + } + + public function test_dai_sistemi_si_aprono_i_modelli_filtrati_per_sistema(): void + { + $pvc = Sistema::where('codice', 'pvc')->firstOrFail(); + $altro = Sistema::create(['codice' => 'legno', 'nome' => 'Legno']); + $modelloAltro = Modello::create(['sistema_id' => $altro->id, 'codice' => 'finestra_1_anta', 'nome' => 'Finestra legno', 'attivo' => false]); + + Livewire::test(ListSistemi::class) + ->assertTableActionHasUrl('modelli', ModelloResource::getUrl('index', [ + 'tableFilters' => ['sistema' => ['value' => $pvc->id]], + ]), $pvc); + + Livewire::withQueryParams(['tableFilters' => ['sistema' => ['value' => $pvc->id]]]) + ->test(ListModelli::class) + ->assertCountTableRecords($pvc->modelli()->count()) + ->assertCanNotSeeTableRecords([$modelloAltro]); + } + + public function test_un_modello_attivo_richiede_la_configurazione_3d(): void + { + $sistema = Modello::firstOrFail()->sistema_id; + + Livewire::test(CreateModello::class) + ->fillForm(['sistema_id' => $sistema, 'codice' => 'nuovo', 'nome' => 'Nuovo', 'attivo' => true]) + ->call('create') + ->assertHasFormErrors(['cartella' => 'required', 'file_modello' => 'required', 'categoria' => 'required']); + + Livewire::test(CreateModello::class) + ->fillForm(['sistema_id' => $sistema, 'codice' => 'finestra_1_anta', 'nome' => 'Doppione', 'attivo' => false]) + ->call('create') + ->assertHasFormErrors(['codice' => 'unique']); + } +} diff --git a/backoffice/tests/Feature/CatalogoApiTest.php b/backoffice/tests/Feature/CatalogoApiTest.php new file mode 100644 index 0000000..0d1550b --- /dev/null +++ b/backoffice/tests/Feature/CatalogoApiTest.php @@ -0,0 +1,71 @@ +seed(CatalogoSeeder::class); + + $pvc = Sistema::where('codice', 'pvc')->firstOrFail(); + $this->assertSame(30, $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')); + } + + public function test_il_seeder_non_sovrascrive_le_modifiche_admin(): void + { + $this->seed(CatalogoSeeder::class); + Modello::where('codice', 'bilico')->update(['nome' => 'Bilico modificato']); + + $this->seed(CatalogoSeeder::class); + + $this->assertSame('Bilico modificato', Modello::where('codice', 'bilico')->value('nome')); + $this->assertSame(30, Modello::count()); + } + + public function test_api_sistemi_espone_solo_modelli_attivi_in_ordine(): void + { + $this->seed(CatalogoSeeder::class); + Sistema::create(['codice' => 'legno', 'nome' => 'Legno', 'attivo' => false]); + + $response = $this->getJson('/api/sistemi')->assertOk(); + + $response->assertJsonCount(1); + $response->assertJsonPath('0.codice', 'pvc'); + + $modelli = collect($response->json('0.modelli')); + $this->assertSame(26, $modelli->count()); + $this->assertSame('finestra_1_anta', $modelli->first()['id']); + $this->assertNotContains('scorrevole_sf_3_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']); + } + + public function test_api_sistema_inesistente_restituisce_404(): void + { + $this->getJson('/api/sistemi/inesistente')->assertNotFound(); + } +} diff --git a/reference/OrdineSerramenti_Descrizioni_NuoveOpzioni.xlsx b/reference/OrdineSerramenti_Descrizioni_NuoveOpzioni.xlsx new file mode 100644 index 0000000..ca63ef4 Binary files /dev/null and b/reference/OrdineSerramenti_Descrizioni_NuoveOpzioni.xlsx differ diff --git a/reference/PVC_LISTINO_aggiornamento2026.xls b/reference/PVC_LISTINO_aggiornamento2026.xls new file mode 100644 index 0000000..8c74e13 Binary files /dev/null and b/reference/PVC_LISTINO_aggiornamento2026.xls differ diff --git a/src/js/menu_builder.js b/src/js/menu_builder.js index 8d951ea..32e768f 100644 --- a/src/js/menu_builder.js +++ b/src/js/menu_builder.js @@ -229,7 +229,7 @@ const buildListaModelli = (modelli, modello, categoria, sistema) => { html += `
- ${m.id} + ${m.id}

${m.nome}

diff --git a/src/js/starter.js b/src/js/starter.js index 4103d77..833c741 100644 --- a/src/js/starter.js +++ b/src/js/starter.js @@ -23,8 +23,6 @@ window.GLTFLoader = GLTFLoader; // Used by APP Scripts. // --- Integrazione backend preventivi --- const CART_TOKEN_KEY = 'kreios_cart_token'; -// Ordine dei sistemi come nel menu: il primo è quello di default -const SISTEMI = ['pvc', 'alluminio', 'legno', 'acciaio']; // 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 @@ -138,15 +136,21 @@ $(() => { const start = async () => { const params = ParamsParser.getQueryParams(); - // Senza sistema in query string si parte dal primo sistema (pvc) - if (!SISTEMI.includes(params.sistema)) - params.sistema = SISTEMI[0]; + // 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 = await (await fetch(`${data_folder}/modelli.json?rnd=` + Math.random())).json(); + 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);