feat: add previous configuration handling for user selections in quotes
- Implemented session storage management for the last configuration choices made by the user. - Added functionality to prompt users when changing models to retain their previous selections. - Introduced methods to apply previous choices to the newly loaded model, including finishes, handles, glass types, and options. - Enhanced user experience by ensuring selections are automatically applied where applicable.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\FinituraResource\Pages;
|
||||
use App\Filament\Support\CampiConfiguratore;
|
||||
use App\Models\Finitura;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class FinituraResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Finitura::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-swatch';
|
||||
|
||||
protected static ?string $slug = 'finiture';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuratore';
|
||||
|
||||
protected static ?string $navigationLabel = 'Finiture e maniglie';
|
||||
|
||||
protected static ?string $modelLabel = 'finitura';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'finiture';
|
||||
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Forms\Components\Section::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
CampiConfiguratore::sistema(),
|
||||
Forms\Components\Select::make('tipo')
|
||||
->label('Tipo')
|
||||
->options(Finitura::TIPI)
|
||||
->default('profilo')
|
||||
->required()
|
||||
->live(),
|
||||
Forms\Components\TextInput::make('codice')
|
||||
->label('Codice')
|
||||
->helperText('Identificativo inviato dal configuratore (es. 2, M1).')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->unique(
|
||||
ignoreRecord: true,
|
||||
modifyRuleUsing: fn (Unique $rule, Get $get) => $rule
|
||||
->where('sistema_id', $get('sistema_id'))
|
||||
->where('tipo', $get('tipo')),
|
||||
),
|
||||
Forms\Components\TextInput::make('descrizione')->label('Descrizione')->required()->maxLength(255),
|
||||
Forms\Components\TextInput::make('codice_mazzetta')->label('Codice mazzetta')->maxLength(50),
|
||||
CampiConfiguratore::immagine('anteprima', 'Anteprima nel menu', 'finiture/anteprime'),
|
||||
Forms\Components\Select::make('lato')
|
||||
->label('Lato')
|
||||
->helperText('Su quale lato del serramento si può scegliere. Si può cambiare per singolo modello dalla scheda del modello.')
|
||||
->options(Finitura::LATI)
|
||||
->default('entrambi')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('tipo') === 'profilo'),
|
||||
Forms\Components\Toggle::make('predefinita')
|
||||
->label('Predefinita')
|
||||
->helperText('Selezionata all\'apertura. Per le finiture del profilo: la stessa finitura predefinita su interno ed esterno è il "bianco" senza supplemento.')
|
||||
->inline(false),
|
||||
Forms\Components\Toggle::make('attivo')
|
||||
->label('Attiva nel sistema')
|
||||
->helperText('Disattiva: non compare nei modelli, salvo quelli che la attivano nella propria scheda.')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
]),
|
||||
CampiConfiguratore::aspetto3d('finiture'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('ordine')
|
||||
->reorderable('ordine')
|
||||
->defaultGroup('tipo')
|
||||
->groups([
|
||||
Tables\Grouping\Group::make('tipo')
|
||||
->label('Tipo')
|
||||
->getTitleFromRecordUsing(fn (Finitura $record) => Finitura::TIPI[$record->tipo] ?? $record->tipo),
|
||||
])
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('anteprima')->label('Anteprima')->disk('public')->height(40),
|
||||
Tables\Columns\TextColumn::make('codice')->label('Codice')->searchable(),
|
||||
Tables\Columns\TextColumn::make('descrizione')->label('Descrizione')->searchable()->wrap(),
|
||||
Tables\Columns\SelectColumn::make('lato')
|
||||
->label('Lato')
|
||||
->options(Finitura::LATI)
|
||||
->selectablePlaceholder(false)
|
||||
->disabled(fn (Finitura $record) => $record->tipo !== 'profilo'),
|
||||
Tables\Columns\ColorColumn::make('colore')->label('Colore'),
|
||||
Tables\Columns\IconColumn::make('texture')
|
||||
->label('Texture')
|
||||
->icon(fn (?string $state) => $state ? 'heroicon-o-photo' : null),
|
||||
Tables\Columns\IconColumn::make('predefinita')->label('Predefinita')->boolean(),
|
||||
Tables\Columns\ToggleColumn::make('attivo')->label('Attiva'),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('sistema')->label('Sistema')->relationship('sistema', 'nome'),
|
||||
Tables\Filters\SelectFilter::make('tipo')->label('Tipo')->options(Finitura::TIPI),
|
||||
Tables\Filters\SelectFilter::make('lato')->label('Lato')->options(Finitura::LATI),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make()->iconButton(),
|
||||
Tables\Actions\DeleteAction::make()->iconButton(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\BulkAction::make('lato')
|
||||
->label('Imposta lato')
|
||||
->icon('heroicon-o-arrows-right-left')
|
||||
->form([
|
||||
Forms\Components\Select::make('lato')->label('Lato')->options(Finitura::LATI)->required(),
|
||||
])
|
||||
->action(fn (Collection $records, array $data) => $records
|
||||
->where('tipo', 'profilo')
|
||||
->each->update(['lato' => $data['lato']]))
|
||||
->deselectRecordsAfterCompletion(),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageFiniture::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FinituraResource\Pages;
|
||||
|
||||
use App\Filament\Resources\FinituraResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageFiniture extends ManageRecords
|
||||
{
|
||||
protected static string $resource = FinituraResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\GruppoOpzioniResource\Pages;
|
||||
use App\Models\GruppoOpzioni;
|
||||
use App\Models\Modello;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
/** Gruppi di opzioni del configuratore: un nuovo gruppo si crea qui, le sue voci in "Opzioni e supplementi". */
|
||||
class GruppoOpzioniResource extends Resource
|
||||
{
|
||||
protected static ?string $model = GruppoOpzioni::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-queue-list';
|
||||
|
||||
protected static ?string $slug = 'gruppi-opzioni';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuratore';
|
||||
|
||||
protected static ?string $navigationLabel = 'Gruppi di opzioni';
|
||||
|
||||
protected static ?string $modelLabel = 'gruppo di opzioni';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'gruppi di opzioni';
|
||||
|
||||
protected static ?int $navigationSort = 24;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Forms\Components\Section::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('codice')
|
||||
->label('Codice')
|
||||
->helperText('Chiave del gruppo nelle opzioni e nel preventivo (es. soglia_ribassata). Non modificarlo se il gruppo ha già opzioni.')
|
||||
->required()
|
||||
->alphaDash()
|
||||
->maxLength(50)
|
||||
->unique(ignoreRecord: true)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn (Forms\Set $set, Forms\Get $get, ?string $state) => blank($get('modulo')) ? $set('modulo', $state) : null),
|
||||
Forms\Components\TextInput::make('etichetta')
|
||||
->label('Titolo nel configuratore')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('ui')
|
||||
->label('Controllo')
|
||||
->options(GruppoOpzioni::UI)
|
||||
->default('select')
|
||||
->required(),
|
||||
Forms\Components\Select::make('sezione')
|
||||
->label('Sezione del menu')
|
||||
->options(GruppoOpzioni::SEZIONI)
|
||||
->default('accessori')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('modulo')
|
||||
->label('Modulo')
|
||||
->helperText('Il gruppo compare nei modelli che hanno questo modulo tra i "Moduli" (scheda del modello).')
|
||||
->required()
|
||||
->alphaDash()
|
||||
->maxLength(50)
|
||||
->notIn(array_keys(Modello::MODULI))
|
||||
->validationMessages(['not_in' => 'È un modulo base: scegline uno dedicato al gruppo.']),
|
||||
Forms\Components\TextInput::make('ordine')->label('Ordine')->numeric()->minValue(0)->default(0),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('ordine')
|
||||
->reorderable('ordine')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('etichetta')->label('Titolo')->description(fn (GruppoOpzioni $record) => $record->codice)->searchable(['etichetta', 'codice']),
|
||||
Tables\Columns\TextColumn::make('ui')->label('Controllo')->formatStateUsing(fn (string $state) => GruppoOpzioni::UI[$state] ?? $state)->wrap(),
|
||||
Tables\Columns\TextColumn::make('sezione')->label('Sezione')->badge()->formatStateUsing(fn (string $state) => ucfirst($state)),
|
||||
Tables\Columns\TextColumn::make('modulo')->label('Modulo')->color('gray'),
|
||||
Tables\Columns\TextColumn::make('opzioni_count')->label('Opzioni')->counts('opzioni'),
|
||||
Tables\Columns\TextColumn::make('modelli')
|
||||
->label('Modelli')
|
||||
->state(fn (GruppoOpzioni $record) => Modello::whereJsonContains('moduli', $record->modulo)->count()),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make()->iconButton(),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->iconButton()
|
||||
->disabled(fn (GruppoOpzioni $record) => $record->codice === 'tipo_finitura' || $record->opzioni()->exists())
|
||||
->tooltip(fn (GruppoOpzioni $record) => $record->opzioni()->exists() ? 'Elimina prima le opzioni del gruppo' : null),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageGruppiOpzioni::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GruppoOpzioniResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GruppoOpzioniResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageGruppiOpzioni extends ManageRecords
|
||||
{
|
||||
protected static string $resource = GruppoOpzioniResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ModelloResource\Pages;
|
||||
use App\Filament\Resources\ModelloResource\RelationManagers\FinitureRelationManager;
|
||||
use App\Filament\Resources\ModelloResource\RelationManagers\ListinoRelationManager;
|
||||
use App\Filament\Resources\ModelloResource\RelationManagers\OpzioniRelationManager;
|
||||
use App\Models\Modello;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
@@ -11,6 +13,7 @@ use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class ModelloResource extends Resource
|
||||
@@ -61,6 +64,11 @@ class ModelloResource extends Resource
|
||||
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()
|
||||
@@ -104,9 +112,20 @@ class ModelloResource extends Resource
|
||||
->helperText('Se vuota si usa la cartella dati.'),
|
||||
Forms\Components\CheckboxList::make('moduli')
|
||||
->label('Moduli')
|
||||
->options(Modello::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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -114,13 +133,24 @@ class ModelloResource extends Resource
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('ordine')
|
||||
// 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),
|
||||
Tables\Columns\TextColumn::make('codice')->label('Codice')->searchable()->copyable(),
|
||||
Tables\Columns\TextColumn::make('nome')->label('Nome')->searchable(),
|
||||
// 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')
|
||||
@@ -131,7 +161,11 @@ class ModelloResource extends Resource
|
||||
->label('Listino')
|
||||
->counts('listino')
|
||||
->formatStateUsing(fn (int $state) => $state ? "{$state} prezzi" : 'Nessuno')
|
||||
->color(fn (int $state) => $state ? null : 'gray')
|
||||
// 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')
|
||||
@@ -142,10 +176,19 @@ class ModelloResource extends Resource
|
||||
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(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
Tables\Actions\EditAction::make()->iconButton(),
|
||||
Tables\Actions\DeleteAction::make()->iconButton(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
@@ -158,6 +201,8 @@ class ModelloResource extends Resource
|
||||
{
|
||||
return [
|
||||
ListinoRelationManager::class,
|
||||
FinitureRelationManager::class,
|
||||
OpzioniRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModelloResource\RelationManagers;
|
||||
|
||||
use App\Models\Finitura;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Finiture del profilo del sistema e, per ciascuna, la scelta del modello: come il
|
||||
* sistema (nessuna riga in finiture_modelli) oppure lato proprio o disattivata.
|
||||
*/
|
||||
class FinitureRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'finitureDelSistema';
|
||||
|
||||
protected static ?string $title = 'Finiture';
|
||||
|
||||
protected static ?string $modelLabel = 'finitura';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'finiture';
|
||||
|
||||
/** Scelte del modello per finitura_id, lette una volta per richiesta. */
|
||||
protected ?\Illuminate\Support\Collection $scelte = null;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->description('Per ogni finitura del profilo scegli se nel modello vale l\'impostazione del sistema (lato e attivazione in "Finiture e maniglie") o una propria: un lato diverso, oppure disattivata. La scelta del modello può anche attivare una finitura disattiva nel sistema.')
|
||||
->defaultSort('ordine')
|
||||
->defaultPaginationPageOption(50)
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('anteprima')->label('Anteprima')->disk('public')->height(36),
|
||||
Tables\Columns\TextColumn::make('descrizione')
|
||||
->label('Finitura')
|
||||
->description(fn (Finitura $record) => $record->codice)
|
||||
->searchable(['descrizione', 'codice']),
|
||||
Tables\Columns\TextColumn::make('sistema')
|
||||
->label('Nel sistema')
|
||||
->state(fn (Finitura $record) => $record->attivo ? Finitura::LATI[$record->lato] : 'Disattiva')
|
||||
->color('gray'),
|
||||
Tables\Columns\SelectColumn::make('scelta_modello')
|
||||
->label('In questo modello')
|
||||
->options(Finitura::LATI_MODELLO)
|
||||
->placeholder('Come il sistema')
|
||||
->getStateUsing(fn (Finitura $record) => $this->scelte()->get($record->id))
|
||||
->updateStateUsing(function (Finitura $record, ?string $state) {
|
||||
$this->salvaScelta([$record->id], $state);
|
||||
|
||||
return $state;
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('effettiva')
|
||||
->label('Risultato')
|
||||
->state(function (Finitura $record) {
|
||||
$lato = $this->scelte()->get($record->id) ?? ($record->attivo ? $record->lato : 'nessuno');
|
||||
|
||||
return $lato === 'nessuno' ? 'Non disponibile' : Finitura::LATI[$lato];
|
||||
})
|
||||
->badge()
|
||||
->color(fn (string $state) => $state === 'Non disponibile' ? 'danger' : 'success'),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\TernaryFilter::make('personalizzata')
|
||||
->label('Scelta del modello')
|
||||
->placeholder('Tutte')
|
||||
->trueLabel('Personalizzate')
|
||||
->falseLabel('Come il sistema')
|
||||
->queries(
|
||||
true: fn ($query) => $query->whereIn('id', $this->scelte()->keys()),
|
||||
false: fn ($query) => $query->whereNotIn('id', $this->scelte()->keys()),
|
||||
),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('ripristina')
|
||||
->label('Tutte come il sistema')
|
||||
->icon('heroicon-o-arrow-uturn-left')
|
||||
->color('gray')
|
||||
->requiresConfirmation()
|
||||
->modalDescription('Elimina le scelte del modello: per tutte le finiture torna a valere l\'impostazione del sistema.')
|
||||
->visible(fn () => $this->scelte()->isNotEmpty())
|
||||
->action(function () {
|
||||
DB::table('finiture_modelli')->where('modello_id', $this->getOwnerRecord()->getKey())->delete();
|
||||
$this->scelte = null;
|
||||
|
||||
Notification::make()->title('Finiture ripristinate come nel sistema')->success()->send();
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkAction::make('imposta')
|
||||
->label('Imposta nel modello')
|
||||
->icon('heroicon-o-adjustments-horizontal')
|
||||
->form([
|
||||
Forms\Components\Select::make('lato')
|
||||
->label('In questo modello')
|
||||
->options(['sistema' => 'Come il sistema'] + Finitura::LATI_MODELLO)
|
||||
->required(),
|
||||
])
|
||||
->action(fn (Collection $records, array $data) => $this->salvaScelta(
|
||||
$records->modelKeys(),
|
||||
$data['lato'] === 'sistema' ? null : $data['lato'],
|
||||
))
|
||||
->deselectRecordsAfterCompletion(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isReadOnly(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function scelte(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->scelte ??= DB::table('finiture_modelli')
|
||||
->where('modello_id', $this->getOwnerRecord()->getKey())
|
||||
->pluck('lato', 'finitura_id');
|
||||
}
|
||||
|
||||
/** null = come il sistema (rimuove la scelta del modello). */
|
||||
private function salvaScelta(array $finiture, ?string $lato): void
|
||||
{
|
||||
$modello = $this->getOwnerRecord()->getKey();
|
||||
$righe = DB::table('finiture_modelli')->where('modello_id', $modello)->whereIn('finitura_id', $finiture);
|
||||
|
||||
if ($lato === null || $lato === '') {
|
||||
$righe->delete();
|
||||
} else {
|
||||
abort_unless(array_key_exists($lato, Finitura::LATI_MODELLO), 422);
|
||||
DB::table('finiture_modelli')->upsert(
|
||||
array_map(fn ($id) => ['modello_id' => $modello, 'finitura_id' => $id, 'lato' => $lato, 'created_at' => now(), 'updated_at' => now()], $finiture),
|
||||
['modello_id', 'finitura_id'],
|
||||
['lato', 'updated_at'],
|
||||
);
|
||||
}
|
||||
|
||||
$this->scelte = null;
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModelloResource\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\OpzioneResource;
|
||||
use App\Models\GruppoOpzioni;
|
||||
use App\Models\Opzione;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Opzioni personalizzate del modello: se il modello ha almeno una riga in un gruppo,
|
||||
* per quel modello il gruppo del sistema è sostituito per intero.
|
||||
*/
|
||||
class OpzioniRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'opzioni';
|
||||
|
||||
protected static ?string $title = 'Opzioni personalizzate';
|
||||
|
||||
protected static ?string $modelLabel = 'opzione';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'opzioni';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form->schema(OpzioneResource::campi(conSistema: false, modello: fn () => $this->getOwnerRecord()));
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return OpzioneResource::tabella($table)
|
||||
->description('I gruppi presenti qui sostituiscono, per questo modello, quelli definiti in "Opzioni e supplementi" del sistema (es. icone di apertura proprie o un supplemento diverso). Eliminando tutte le righe di un gruppo torna a valere quello del sistema.')
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('personalizza')
|
||||
->label('Personalizza gruppo')
|
||||
->icon('heroicon-o-document-duplicate')
|
||||
->color('gray')
|
||||
->modalDescription('Copia nel modello le opzioni del gruppo definite sul sistema, da modificare qui.')
|
||||
->form([
|
||||
Forms\Components\Select::make('gruppo')
|
||||
->label('Gruppo')
|
||||
->options(fn () => $this->gruppiPersonalizzabili())
|
||||
->required(),
|
||||
])
|
||||
->action(function (array $data) {
|
||||
$copiate = $this->copiaGruppo($data['gruppo']);
|
||||
|
||||
Notification::make()->title("Copiate {$copiate} opzioni dal sistema")->success()->send();
|
||||
}),
|
||||
Tables\Actions\CreateAction::make()
|
||||
->mutateFormDataUsing(fn (array $data) => $this->conIconaDelSistema($data) + ['sistema_id' => $this->getOwnerRecord()->sistema_id]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isReadOnly(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Gruppi del sistema non ancora personalizzati per il modello. */
|
||||
private function gruppiPersonalizzabili(): array
|
||||
{
|
||||
$modello = $this->getOwnerRecord();
|
||||
$personalizzati = $modello->opzioni()->distinct()->pluck('gruppo')->all();
|
||||
|
||||
return Opzione::query()
|
||||
->where('sistema_id', $modello->sistema_id)
|
||||
->whereNull('modello_id')
|
||||
->whereNotIn('gruppo', $personalizzati)
|
||||
->distinct()
|
||||
->pluck('gruppo')
|
||||
->mapWithKeys(fn (string $g) => [$g => GruppoOpzioni::where('codice', $g)->value('etichetta') ?? $g])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function copiaGruppo(string $gruppo): int
|
||||
{
|
||||
$modello = $this->getOwnerRecord();
|
||||
|
||||
$righe = Opzione::query()
|
||||
->where('sistema_id', $modello->sistema_id)
|
||||
->whereNull('modello_id')
|
||||
->where('gruppo', $gruppo)
|
||||
->get();
|
||||
|
||||
DB::transaction(function () use ($righe, $modello) {
|
||||
foreach ($righe as $riga) {
|
||||
$copia = $riga->replicate(['icona']);
|
||||
$copia->modello_id = $modello->getKey();
|
||||
$copia->icona = $this->copiaIcona($riga);
|
||||
$copia->save();
|
||||
}
|
||||
});
|
||||
|
||||
return $righe->count();
|
||||
}
|
||||
|
||||
/** Nuova opzione con un valore del sistema e senza icona: prende una copia di quella del sistema. */
|
||||
private function conIconaDelSistema(array $data): array
|
||||
{
|
||||
if (blank($data['icona'] ?? null)) {
|
||||
$origine = OpzioneResource::catalogo($this->getOwnerRecord(), $data['gruppo'] ?? null)->firstWhere('valore', $data['valore'] ?? null);
|
||||
$data['icona'] = $origine ? $this->copiaIcona($origine) : null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** Icona duplicata: ogni record ha i suoi file (vedi HaImmaginiCatalogo). */
|
||||
private function copiaIcona(Opzione $origine): ?string
|
||||
{
|
||||
$disco = Storage::disk('public');
|
||||
if (! $origine->icona || ! $disco->exists($origine->icona)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$percorso = 'configuratore/opzioni/'.Str::random(20).'.'.pathinfo($origine->icona, PATHINFO_EXTENSION);
|
||||
$disco->copy($origine->icona, $percorso);
|
||||
|
||||
return $percorso;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\OpzioneResource\Pages;
|
||||
use App\Filament\Support\CampiConfiguratore;
|
||||
use App\Models\GruppoOpzioni;
|
||||
use App\Models\Modello;
|
||||
use App\Models\Opzione;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
/** Opzioni e supplementi del sistema (le personalizzazioni per modello sono nella scheda del modello). */
|
||||
class OpzioneResource extends Resource
|
||||
{
|
||||
/** Voce "Altro" del valore nelle opzioni personalizzate: valore scritto a mano. */
|
||||
public const ALTRO = '__altro';
|
||||
|
||||
protected static ?string $model = Opzione::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-adjustments-horizontal';
|
||||
|
||||
protected static ?string $slug = 'opzioni';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuratore';
|
||||
|
||||
protected static ?string $navigationLabel = 'Opzioni e supplementi';
|
||||
|
||||
protected static ?string $modelLabel = 'opzione';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'opzioni';
|
||||
|
||||
protected static ?int $navigationSort = 23;
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()->whereNull('modello_id');
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema(self::campi(conSistema: true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Campi dell'opzione; senza sistema (relation manager del modello) l'unicità è sul modello
|
||||
* e il valore si sceglie tra quelli del gruppo sul sistema, o "Altro" per scriverlo a mano.
|
||||
*
|
||||
* @param \Closure(): Modello|null $modello
|
||||
*/
|
||||
public static function campi(bool $conSistema, ?\Closure $modello = null): array
|
||||
{
|
||||
$unicita = fn (Unique $rule, Get $get) => self::unicita($rule, $get, $modello);
|
||||
$finitura = fn (Get $get) => $get('gruppo') === 'tipo_finitura';
|
||||
|
||||
return [
|
||||
Forms\Components\Section::make()
|
||||
->columns(2)
|
||||
->schema(array_values(array_filter([
|
||||
$conSistema ? CampiConfiguratore::sistema() : null,
|
||||
Forms\Components\Select::make('gruppo')
|
||||
->label('Gruppo')
|
||||
->options(fn () => GruppoOpzioni::etichette())
|
||||
->required()
|
||||
->live()
|
||||
->afterStateUpdated(function (Set $set) {
|
||||
$set('valore', null);
|
||||
$set('valore_altro', null);
|
||||
}),
|
||||
$modello ? null : Forms\Components\Select::make('valore')
|
||||
->label('Valore')
|
||||
->options(Opzione::TIPI_FINITURA)
|
||||
->helperText('Il tipo finitura è calcolato dalle finiture interna ed esterna scelte.')
|
||||
->visible($finitura)
|
||||
->required()
|
||||
->unique(ignoreRecord: true, modifyRuleUsing: $unicita),
|
||||
$modello ? null : Forms\Components\TextInput::make('valore')
|
||||
->label('Valore')
|
||||
->helperText('Identificativo inviato dal configuratore (es. dx, telaio_l).')
|
||||
->hidden($finitura)
|
||||
->required()
|
||||
->alphaDash()
|
||||
->maxLength(100)
|
||||
->unique(ignoreRecord: true, modifyRuleUsing: $unicita),
|
||||
$modello ? Forms\Components\Select::make('valore')
|
||||
->label('Valore')
|
||||
->helperText(fn (Get $get) => $finitura($get)
|
||||
? 'Il tipo finitura è calcolato dalle finiture interna ed esterna scelte.'
|
||||
: 'Valori del gruppo definiti sul sistema: sceglierne uno ne precompila i dati. "Altro" per un valore nuovo.')
|
||||
// Un solo campo per tutti i gruppi: con due campi "valore" Filament aggiorna solo il primo.
|
||||
->options(fn (Get $get) => $finitura($get)
|
||||
? Opzione::TIPI_FINITURA
|
||||
: self::catalogo($modello(), $get('gruppo'))
|
||||
->mapWithKeys(fn (Opzione $o) => [$o->valore => "{$o->etichetta} ({$o->valore})"])
|
||||
->put(self::ALTRO, 'Altro…')
|
||||
->all())
|
||||
->required()
|
||||
->live()
|
||||
->unique(ignoreRecord: true, modifyRuleUsing: $unicita)
|
||||
// Valore non presente sul sistema: in modifica compare come "Altro".
|
||||
->afterStateHydrated(function (Forms\Components\Select $component, ?string $state, Get $get, Set $set) use ($modello) {
|
||||
if (filled($state) && $get('gruppo') !== 'tipo_finitura'
|
||||
&& ! self::catalogo($modello(), $get('gruppo'))->contains('valore', $state)) {
|
||||
$component->state(self::ALTRO);
|
||||
$set('valore_altro', $state);
|
||||
}
|
||||
})
|
||||
->afterStateUpdated(fn (?string $state, Get $get, Set $set) => self::precompila($modello(), $get('gruppo'), $state, $set))
|
||||
->dehydrateStateUsing(fn (?string $state, Get $get) => $state === self::ALTRO ? $get('valore_altro') : $state) : null,
|
||||
$modello ? Forms\Components\TextInput::make('valore_altro')
|
||||
->label('Nuovo valore')
|
||||
->helperText('Identificativo inviato dal configuratore (es. dx, telaio_l).')
|
||||
->visible(fn (Get $get) => $get('valore') === self::ALTRO)
|
||||
->required()
|
||||
->alphaDash()
|
||||
->maxLength(100)
|
||||
->notIn([self::ALTRO])
|
||||
->unique(table: Opzione::class, column: 'valore', ignoreRecord: true, modifyRuleUsing: $unicita)
|
||||
->dehydrated(false) : null,
|
||||
Forms\Components\TextInput::make('etichetta')->label('Etichetta')->required()->maxLength(255),
|
||||
Forms\Components\TextInput::make('alternativa')
|
||||
->label('Alternativa a')
|
||||
->helperText('Le opzioni del gruppo con lo stesso valore si escludono a vicenda (es. "coprifili": piatti o scatolati).')
|
||||
->maxLength(50)
|
||||
->visible(fn (Get $get) => GruppoOpzioni::where('codice', $get('gruppo'))->value('ui') === 'multipla'),
|
||||
Forms\Components\Toggle::make('attivo')->label('Attiva')->default(true)->inline(false),
|
||||
]))),
|
||||
Forms\Components\Section::make('Supplemento')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\Select::make('supplemento_tipo')
|
||||
->label('Tipo')
|
||||
->options(Opzione::SUPPLEMENTI)
|
||||
->placeholder('Nessun supplemento')
|
||||
->live(),
|
||||
Forms\Components\TextInput::make('supplemento')
|
||||
->label('Valore')
|
||||
->numeric()
|
||||
->step(0.01)
|
||||
->default(0)
|
||||
->required()
|
||||
->prefix(fn (Get $get) => in_array($get('supplemento_tipo'), ['fisso', 'metro_lineare'], true) ? '€' : null)
|
||||
->suffix(fn (Get $get) => match ($get('supplemento_tipo')) {
|
||||
'percentuale' => '%',
|
||||
'metro_lineare' => '/ml',
|
||||
default => null,
|
||||
})
|
||||
->visible(fn (Get $get) => filled($get('supplemento_tipo'))),
|
||||
Forms\Components\Fieldset::make('Metri lineari')
|
||||
->columns(3)
|
||||
->visible(fn (Get $get) => $get('supplemento_tipo') === 'metro_lineare')
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('formula_ml')
|
||||
->hiddenLabel()
|
||||
->content('ml = (L × moltiplicatore L + H × moltiplicatore H + aggiunta) ÷ 1000, con L e H del serramento in mm. Es. "H + 100 mm, 2 pezzi": moltiplicatore H 2, aggiunta 200.')
|
||||
->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('ml_per_larghezza')->label('Moltiplicatore L')->numeric()->step(0.001)->default(0)->required(),
|
||||
Forms\Components\TextInput::make('ml_per_altezza')->label('Moltiplicatore H')->numeric()->step(0.001)->default(0)->required(),
|
||||
Forms\Components\TextInput::make('ml_aggiunta_mm')->label('Aggiunta')->numeric()->step(1)->suffix('mm')->default(0)->required(),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Configuratore')
|
||||
->columns(2)
|
||||
->collapsible()
|
||||
->hidden(fn (Get $get) => $get('gruppo') === 'tipo_finitura')
|
||||
->schema([
|
||||
CampiConfiguratore::immagine('icona', 'Icona', 'opzioni'),
|
||||
Forms\Components\KeyValue::make('parametri')
|
||||
->label('Parametri')
|
||||
->helperText('Letti dal configuratore: scale_x (1 / -1) per l\'apertura, func e categoria per le select.')
|
||||
->keyLabel('Nome')
|
||||
->valueLabel('Valore'),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return self::tabella($table)
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('sistema')->label('Sistema')->relationship('sistema', 'nome'),
|
||||
Tables\Filters\SelectFilter::make('gruppo')->label('Gruppo')->options(fn () => GruppoOpzioni::etichette()),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Tabella condivisa con le opzioni personalizzate del modello. */
|
||||
public static function tabella(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('ordine')
|
||||
->reorderable('ordine')
|
||||
->defaultGroup('gruppo')
|
||||
->groups([
|
||||
Tables\Grouping\Group::make('gruppo')
|
||||
->label('Gruppo')
|
||||
->getTitleFromRecordUsing(fn (Opzione $record) => $record->definizione?->etichetta ?? $record->gruppo),
|
||||
])
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('icona')->label('Icona')->disk('public')->height(40),
|
||||
Tables\Columns\TextColumn::make('etichetta')->label('Etichetta')->searchable(),
|
||||
Tables\Columns\TextColumn::make('valore')->label('Valore')->searchable()->color('gray'),
|
||||
Tables\Columns\TextColumn::make('alternativa')->label('Alternativa')->badge()->color('gray')->placeholder('—')->toggleable(),
|
||||
Tables\Columns\TextColumn::make('supplemento')
|
||||
->label('Supplemento')
|
||||
->state(fn (Opzione $record) => $record->descriviSupplemento()),
|
||||
Tables\Columns\ToggleColumn::make('attivo')->label('Attiva'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make()->iconButton(),
|
||||
Tables\Actions\DeleteAction::make()->iconButton(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageOpzioni::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
private static function unicita(Unique $rule, Get $get, ?\Closure $modello): Unique
|
||||
{
|
||||
$rule->where('gruppo', $get('gruppo'));
|
||||
|
||||
return $modello
|
||||
? $rule->where('modello_id', $modello()->getKey())
|
||||
: $rule->where('sistema_id', $get('sistema_id'))->whereNull('modello_id');
|
||||
}
|
||||
|
||||
/** Opzioni del gruppo definite sul sistema del modello. */
|
||||
public static function catalogo(Modello $modello, ?string $gruppo): Collection
|
||||
{
|
||||
return Opzione::query()
|
||||
->where('sistema_id', $modello->sistema_id)
|
||||
->whereNull('modello_id')
|
||||
->where('gruppo', $gruppo)
|
||||
->orderBy('ordine')
|
||||
->get();
|
||||
}
|
||||
|
||||
/** Copia nel form i dati dell'opzione del sistema scelta come valore. */
|
||||
private static function precompila(Modello $modello, ?string $gruppo, ?string $valore, Set $set): void
|
||||
{
|
||||
$set('valore_altro', null);
|
||||
$origine = self::catalogo($modello, $gruppo)->firstWhere('valore', $valore);
|
||||
if (! $origine) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['etichetta', 'alternativa', 'supplemento_tipo', 'supplemento', 'ml_per_larghezza', 'ml_per_altezza', 'ml_aggiunta_mm', 'parametri'] as $campo) {
|
||||
$set($campo, $origine->{$campo});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OpzioneResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OpzioneResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageOpzioni extends ManageRecords
|
||||
{
|
||||
protected static string $resource = OpzioneResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\RegolaVetroResource\Pages;
|
||||
use App\Filament\Support\CampiConfiguratore;
|
||||
use App\Models\RegolaVetro;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class RegolaVetroResource extends Resource
|
||||
{
|
||||
protected static ?string $model = RegolaVetro::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-calculator';
|
||||
|
||||
protected static ?string $slug = 'regole-vetro';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuratore';
|
||||
|
||||
protected static ?string $navigationLabel = 'Regole vetro';
|
||||
|
||||
protected static ?string $modelLabel = 'regola vetro';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'regole vetro';
|
||||
|
||||
protected static ?int $navigationSort = 22;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Forms\Components\Section::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
CampiConfiguratore::sistema(),
|
||||
Forms\Components\TextInput::make('codice')
|
||||
->label('Codice')
|
||||
->helperText('Es. F2, TR_A_F.')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->unique(
|
||||
ignoreRecord: true,
|
||||
modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('sistema_id', $get('sistema_id')),
|
||||
),
|
||||
Forms\Components\TextInput::make('descrizione')->label('Tipologia')->required()->maxLength(255)->columnSpanFull(),
|
||||
]),
|
||||
Forms\Components\Section::make('Lastre')
|
||||
->description('Per ogni riga: larghezza vetro LV = (L − detrazione) ÷ divisore, altezza vetro HV = H − detrazione, ripetuta per la quantità. Es. L/2 − 181 si scrive detrazione 362, divisore 2.')
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('lastre')
|
||||
->hiddenLabel()
|
||||
->columns(5)
|
||||
->minItems(1)
|
||||
->defaultItems(1)
|
||||
->reorderable(false)
|
||||
->live(onBlur: true)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('descrizione')->label('Descrizione')->placeholder('es. Vetro anta')->maxLength(100),
|
||||
Forms\Components\TextInput::make('quantita')->label('Quantità')->integer()->minValue(1)->default(1)->required(),
|
||||
Forms\Components\TextInput::make('divisore')->label('Divisore L')->integer()->minValue(1)->default(1)->required(),
|
||||
Forms\Components\TextInput::make('detrazione_larghezza')->label('Detrazione L (mm)')->numeric()->default(0)->required(),
|
||||
Forms\Components\TextInput::make('detrazione_altezza')->label('Detrazione H (mm)')->numeric()->default(0)->required(),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Prova')
|
||||
->description('Verifica la regola su una misura: non viene salvata.')
|
||||
->columns(3)
|
||||
->collapsible()
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('prova_larghezza')->label('Larghezza L (mm)')->integer()->default(1200)->dehydrated(false)->live(onBlur: true),
|
||||
Forms\Components\TextInput::make('prova_altezza')->label('Altezza H (mm)')->integer()->default(1400)->dehydrated(false)->live(onBlur: true),
|
||||
Forms\Components\Placeholder::make('prova_risultato')
|
||||
->label('Lastre risultanti')
|
||||
->content(fn (Get $get) => self::anteprima($get('lastre') ?? [], (float) $get('prova_larghezza'), (float) $get('prova_altezza'))),
|
||||
]),
|
||||
Forms\Components\Textarea::make('note')->label('Note')->rows(2)->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('codice')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('codice')->label('Codice')->searchable()->sortable(),
|
||||
Tables\Columns\TextColumn::make('descrizione')->label('Tipologia')->searchable()->wrap(),
|
||||
Tables\Columns\TextColumn::make('lastre')
|
||||
->label('Formula')
|
||||
->state(fn (RegolaVetro $record) => self::formula($record->lastre ?? []))
|
||||
->html(),
|
||||
Tables\Columns\TextColumn::make('modelli_count')->label('Modelli')->counts('modelli'),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('sistema')->label('Sistema')->relationship('sistema', 'nome'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make()->iconButton(),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->iconButton()
|
||||
->modalDescription('I modelli che usano questa regola non avranno più variazione di prezzo per il vetro.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageRegoleVetro::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
/** Formula leggibile, una riga per lastra (es. "2 × (L − 312) ÷ 2 × (H − 189)"). */
|
||||
private static function formula(array $lastre): HtmlString
|
||||
{
|
||||
$righe = array_map(function (array $l) {
|
||||
$larghezza = 'L'.self::meno($l['detrazione_larghezza'] ?? 0);
|
||||
if ((int) ($l['divisore'] ?? 1) > 1) {
|
||||
$larghezza = "({$larghezza}) ÷ {$l['divisore']}";
|
||||
}
|
||||
$descrizione = filled($l['descrizione'] ?? null) ? e($l['descrizione']).': ' : '';
|
||||
|
||||
return $descrizione.((int) ($l['quantita'] ?? 1)).' × '.$larghezza.' × (H'.self::meno($l['detrazione_altezza'] ?? 0).')';
|
||||
}, $lastre);
|
||||
|
||||
return new HtmlString(implode('<br>', $righe));
|
||||
}
|
||||
|
||||
private static function meno(mixed $detrazione): string
|
||||
{
|
||||
return (float) $detrazione ? ' − '.(float) $detrazione : '';
|
||||
}
|
||||
|
||||
private static function anteprima(array $lastre, float $larghezza, float $altezza): HtmlString
|
||||
{
|
||||
if ($larghezza <= 0 || $altezza <= 0 || ! $lastre) {
|
||||
return new HtmlString('—');
|
||||
}
|
||||
|
||||
$calcolate = (new RegolaVetro(['lastre' => array_values($lastre)]))->calcolaLastre($larghezza, $altezza);
|
||||
|
||||
return new HtmlString(implode('<br>', array_map(fn (array $l) => sprintf(
|
||||
'%s%d × %s × %s mm = %s m² cad.',
|
||||
filled($l['descrizione']) ? e($l['descrizione']).': ' : '',
|
||||
$l['quantita'],
|
||||
number_format($l['larghezza'], 1, ',', '.'),
|
||||
number_format($l['altezza'], 1, ',', '.'),
|
||||
number_format(max(0, $l['larghezza']) * max(0, $l['altezza']) / 1_000_000, 3, ',', '.'),
|
||||
), $calcolate)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RegolaVetroResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RegolaVetroResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageRegoleVetro extends ManageRecords
|
||||
{
|
||||
protected static string $resource = RegolaVetroResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\VetroResource\Pages;
|
||||
use App\Filament\Support\CampiConfiguratore;
|
||||
use App\Models\Vetro;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class VetroResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Vetro::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-square-2-stack';
|
||||
|
||||
protected static ?string $slug = 'vetri';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuratore';
|
||||
|
||||
protected static ?string $navigationLabel = 'Vetri e riempimenti';
|
||||
|
||||
protected static ?string $modelLabel = 'vetro';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'vetri';
|
||||
|
||||
protected static ?int $navigationSort = 21;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Forms\Components\Section::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
CampiConfiguratore::sistema(),
|
||||
Forms\Components\TextInput::make('codice')
|
||||
->label('Codice')
|
||||
->helperText('Identificativo inviato dal configuratore (es. V3).')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->unique(
|
||||
ignoreRecord: true,
|
||||
modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('sistema_id', $get('sistema_id')),
|
||||
),
|
||||
Forms\Components\TextInput::make('descrizione')->label('Descrizione')->required()->maxLength(255)->columnSpanFull(),
|
||||
Forms\Components\Toggle::make('predefinito')->label('Predefinito')->helperText('Selezionato all\'apertura del configuratore.')->inline(false),
|
||||
Forms\Components\Toggle::make('attivo')->label('Attivo')->default(true)->inline(false),
|
||||
]),
|
||||
Forms\Components\Section::make('Prezzo')
|
||||
->description('Variazione = somma per lastra di max(area, minimo fatturabile) x maggiorazione. Le lastre sono calcolate dalla regola vetro del modello.')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('maggiorazione_mq')
|
||||
->label('Maggiorazione')
|
||||
->helperText('Negativa per una decurtazione (es. infisso senza vetro).')
|
||||
->numeric()
|
||||
->step(0.01)
|
||||
->prefix('€')
|
||||
->suffix('/m²')
|
||||
->default(0)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('minimo_mq')
|
||||
->label('Minimo fatturabile per lastra')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->step(0.001)
|
||||
->suffix('m²')
|
||||
->default(0)
|
||||
->required(),
|
||||
]),
|
||||
Forms\Components\Section::make('Immagini')
|
||||
->columns(2)
|
||||
->schema([
|
||||
CampiConfiguratore::immagine('anteprima', 'Icona nel menu', 'vetri'),
|
||||
CampiConfiguratore::immagine('popup', 'Immagine informativa (popup)', 'vetri'),
|
||||
]),
|
||||
CampiConfiguratore::aspetto3d('vetri', opacita: true),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('ordine')
|
||||
->reorderable('ordine')
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('anteprima')->label('Icona')->disk('public')->height(40),
|
||||
Tables\Columns\TextColumn::make('codice')->label('Codice')->searchable(),
|
||||
Tables\Columns\TextColumn::make('descrizione')->label('Descrizione')->searchable()->wrap(),
|
||||
Tables\Columns\TextColumn::make('maggiorazione_mq')
|
||||
->label('Maggiorazione')
|
||||
->formatStateUsing(fn ($state) => '€ '.number_format((float) $state, 2, ',', '.').'/m²')
|
||||
->color(fn ($state) => $state < 0 ? 'danger' : null)
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('minimo_mq')
|
||||
->label('Minimo')
|
||||
->formatStateUsing(fn ($state) => number_format((float) $state, 2, ',', '.').' m²'),
|
||||
Tables\Columns\IconColumn::make('predefinito')->label('Predefinito')->boolean(),
|
||||
Tables\Columns\ToggleColumn::make('attivo')->label('Attivo'),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('sistema')->label('Sistema')->relationship('sistema', 'nome'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make()->iconButton(),
|
||||
Tables\Actions\DeleteAction::make()->iconButton(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageVetri::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\VetroResource\Pages;
|
||||
|
||||
use App\Filament\Resources\VetroResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageVetri extends ManageRecords
|
||||
{
|
||||
protected static string $resource = VetroResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user