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\TextInput::make('titolo') ->label('Titolo nel configuratore') ->helperText('Intestazione del menu e nome nel preventivo; se vuoto si usa la descrizione.') ->maxLength(255) ->columnSpanFull(), 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 (con il modello .glb).') ->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\Placeholder::make('modello_3d_attuale') ->key('modello_3d_attuale') ->label('Modello 3D attuale') ->visible(fn (?Modello $record) => (bool) $record?->modello_3d_draco) ->content(fn (Modello $record) => self::descriviModello3d($record)) ->hintActions([ Forms\Components\Actions\Action::make('scarica_originale') ->label('Originale') ->icon('heroicon-m-arrow-down-tray') ->visible(fn (?Modello $record) => (bool) $record?->modello_3d) ->action(fn (Modello $record) => Storage::disk('local')->download( $record->modello_3d, $record->modello_3d_nome ?: basename($record->modello_3d), )), Forms\Components\Actions\Action::make('scarica_draco') ->label('Compresso') ->icon('heroicon-m-arrow-down-tray') ->url(fn (Modello $record) => $record->modello_3d_url, shouldOpenInNewTab: true), ]) ->columnSpanFull(), Forms\Components\FileUpload::make('modello_3d_upload') ->label(fn (?Modello $record) => $record?->modello_3d_draco ? 'Sostituisci modello 3D' : 'Modello 3D (.glb)') ->helperText('File .glb non compresso. Al salvataggio viene compresso con Draco per il configuratore; l\'originale resta archiviato nel backoffice.') // Il file lo gestisce Modelli3d (vedi Pages\Concerns\CaricaModello3d), non Filament. ->storeFiles(false) ->maxSize(fn () => config('configuratore.modelli_3d.max_mb') * 1024) ->required(fn (Get $get, ?Modello $record) => (bool) $get('attivo') && ! $record?->modello_3d_draco) ->rule(fn () => function (string $attribute, mixed $file, Closure $fail) { try { app(Modelli3d::class)->verificaGlb($file->getRealPath()); } catch (Modello3dNonValido $e) { $fail($e->getMessage()); } }) ->columnSpanFull(), 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') ->helperText('Sezioni del menu del configuratore e voci che entrano nel prezzo.') ->options(fn () => Modello::moduliDisponibili()) ->columns(3) ->columnSpanFull(), Forms\Components\Select::make('regola_vetro_id') ->label('Regola vetro') ->helperText('Geometria delle lastre per la variazione di prezzo del vetro. Vuota: nessuna variazione.') ->relationship( 'regolaVetro', 'descrizione', modifyQueryUsing: fn (Builder $query, Get $get) => $query->where('sistema_id', $get('sistema_id'))->orderBy('codice'), ) ->getOptionLabelFromRecordUsing(fn ($record) => "{$record->codice} — {$record->descrizione}") ->preload(), ]), ]); } public static function table(Table $table): Table { return $table // Ordine del catalogo (quello del configuratore), con i disattivi in fondo. ->defaultSort(fn (Builder $query) => $query->orderByDesc('attivo')->orderBy('ordine')) ->reorderable('ordine') ->reorderRecordsTriggerAction(fn (Tables\Actions\Action $action, bool $isReordering) => $action ->button() ->icon($isReordering ? 'heroicon-m-check' : 'heroicon-m-arrows-up-down') ->label($isReordering ? 'Fine riordino' : 'Riordina')) ->defaultGroup('sistema.nome') ->columns([ Tables\Columns\ImageColumn::make('icona')->label('Icona')->disk('public')->height(48), // Nome a capo su due righe e codice sotto, nella stessa cella: evita lo // scroll orizzontale (i codici, senza spazi, non possono andare a capo da soli). Tables\Columns\TextColumn::make('nome') ->label('Nome / codice') ->description(fn (Modello $record) => $record->codice) ->searchable(['nome', 'codice']) ->wrap() ->extraAttributes(['style' => 'min-width: 11rem; max-width: 14rem; overflow-wrap: anywhere']), 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\IconColumn::make('modello_3d_draco') ->label('3D') ->boolean() ->getStateUsing(fn (Modello $record) => (bool) $record->modello_3d_draco) ->falseColor('danger') ->tooltip(fn (Modello $record) => $record->modello_3d_draco ? $record->modello_3d_nome : 'Senza modello 3D: non compare nel configuratore'), Tables\Columns\TextColumn::make('listino_count') ->label('Listino') ->counts('listino') ->formatStateUsing(fn (int $state) => $state ? "{$state} prezzi" : 'Nessuno') // Senza listino il modello non compare nel configuratore. ->badge(fn (int $state) => ! $state) ->color(fn (int $state) => $state ? null : 'danger') ->icon(fn (int $state) => $state ? null : 'heroicon-m-exclamation-triangle') ->tooltip(fn (int $state) => $state ? null : 'Senza listino: non compare nel configuratore') ->description(fn (Modello $record) => $record->listino_aggiornato_at?->format('d/m/Y')) ->sortable(), Tables\Columns\ToggleColumn::make('attivo') ->label('Attivo') ->disabled(fn (Modello $record) => ! $record->attivo && (! $record->cartella || ! $record->modello_3d_draco)), ]) ->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'), Tables\Filters\TernaryFilter::make('listino') ->label('Listino') ->trueLabel('Con listino') ->falseLabel('Senza listino (non nel configuratore)') ->queries( true: fn (Builder $query) => $query->whereHas('listino'), false: fn (Builder $query) => $query->whereDoesntHave('listino'), ), ]) // Solo icone (con tooltip): le etichette allargherebbero la tabella. ->actions([ Tables\Actions\EditAction::make()->iconButton(), Tables\Actions\DeleteAction::make()->iconButton(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } /** Es. "finestra_2_ante.glb — 10,8 MB → 0,3 MB con Draco (-97%), caricato il 01/11/2026 10:00". */ private static function descriviModello3d(Modello $record): string { $mb = fn (?int $byte) => Number::fileSize((int) $byte, precision: 1); $riduzione = $record->modello_3d_dimensione ? ' (-'.round(100 - $record->modello_3d_draco_dimensione * 100 / $record->modello_3d_dimensione).'%)' : ''; return ($record->modello_3d_nome ?: basename((string) $record->modello_3d_draco)) .' — '.$mb($record->modello_3d_dimensione).' → '.$mb($record->modello_3d_draco_dimensione).' con Draco'.$riduzione .($record->modello_3d_aggiornato_at ? ', caricato il '.$record->modello_3d_aggiornato_at->format('d/m/Y H:i') : ''); } public static function getRelations(): array { return [ ListinoRelationManager::class, FinitureRelationManager::class, OpzioniRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => Pages\ListModelli::route('/'), 'create' => Pages\CreateModello::route('/create'), 'edit' => Pages\EditModello::route('/{record}/edit'), ]; } }