From 863e6ca92050e34ba1907a89516d842003a89dd1 Mon Sep 17 00:00:00 2001 From: Carmine Savino Date: Mon, 19 Oct 2026 17:01:18 +0200 Subject: [PATCH] feat: Aggiungi supporto per prezzi personalizzati nel listino e modifica la logica di importazione --- .../ListinoRelationManager.php | 9 ++- .../Actions/ImportaListiniAction.php | 11 +++- backoffice/app/Models/ListinoPrezzo.php | 3 +- .../app/Support/Listini/ListinoImporter.php | 22 +++++-- ...personalizzato_to_listino_prezzi_table.php | 23 ++++++++ .../tests/Feature/ImportazioneListiniTest.php | 57 +++++++++++++++++++ 6 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 backoffice/database/migrations/2026_10_19_160000_add_personalizzato_to_listino_prezzi_table.php diff --git a/backoffice/app/Filament/Resources/ModelloResource/RelationManagers/ListinoRelationManager.php b/backoffice/app/Filament/Resources/ModelloResource/RelationManagers/ListinoRelationManager.php index b17b3b6..0ebdaea 100644 --- a/backoffice/app/Filament/Resources/ModelloResource/RelationManagers/ListinoRelationManager.php +++ b/backoffice/app/Filament/Resources/ModelloResource/RelationManagers/ListinoRelationManager.php @@ -82,13 +82,20 @@ class ListinoRelationManager extends RelationManager ->step('0.01') ->rules(['required', 'numeric', 'min:0']) ->sortable(), + Tables\Columns\IconColumn::make('personalizzato') + ->label('Personalizzato') + ->icon(fn (bool $state) => $state ? 'heroicon-o-pencil-square' : null) + ->color('warning') + ->tooltip(fn (ListinoPrezzo $record) => $record->personalizzato ? 'Aggiunto a mano: può essere preservato alla prossima importazione' : null), ]) ->filters([ Tables\Filters\SelectFilter::make('altezza')->label('Altezza')->options($misure('altezza')), Tables\Filters\SelectFilter::make('larghezza')->label('Larghezza')->options($misure('larghezza')), + Tables\Filters\TernaryFilter::make('personalizzato')->label('Personalizzato'), ]) ->headerActions([ - Tables\Actions\CreateAction::make(), + Tables\Actions\CreateAction::make() + ->mutateFormDataUsing(fn (array $data) => $data + ['personalizzato' => true]), Tables\Actions\Action::make('svuota') ->label('Svuota listino') ->icon('heroicon-o-trash') diff --git a/backoffice/app/Filament/Resources/SistemaResource/Actions/ImportaListiniAction.php b/backoffice/app/Filament/Resources/SistemaResource/Actions/ImportaListiniAction.php index f2a2d55..7e9c165 100644 --- a/backoffice/app/Filament/Resources/SistemaResource/Actions/ImportaListiniAction.php +++ b/backoffice/app/Filament/Resources/SistemaResource/Actions/ImportaListiniAction.php @@ -57,7 +57,7 @@ class ImportaListiniAction ->schema([ Forms\Components\Placeholder::make('istruzioni') ->hiddenLabel() - ->content('Per ogni tabella trovata nel file scegli il modello a cui assegnare il listino. Le tabelle senza modello non vengono importate; il listino dei modelli associati viene sostituito.'), + ->content('Per ogni tabella trovata nel file scegli il modello a cui assegnare il listino. Le tabelle senza modello non vengono importate; i modelli non associati mantengono il listino attuale.'), // Intestazione unica: le righe sotto sono senza etichette, in forma di tabella. Forms\Components\Grid::make(5) ->extraAttributes(['class' => 'px-4 text-sm font-medium text-gray-950 dark:text-white']) @@ -92,6 +92,13 @@ class ImportaListiniAction ->distinct() ->columnSpan(2), ]), + Forms\Components\Toggle::make('sovrascrivi_tutto') + ->label('Sovrascrivi tutti i prezzi') + ->helperText(fn (Get $get) => $get('sovrascrivi_tutto') + ? 'L\'Excel è la fonte primaria: il listino dei modelli associati viene sostituito per intero, compresi i prezzi aggiunti a mano.' + : 'I prezzi aggiunti a mano (pulsante "Nuovo" nel listino del modello) vengono mantenuti e prevalgono sull\'Excel per la stessa misura.') + ->default(true) + ->live(), ]), ]) ->action(function (array $data, Sistema $record) { @@ -100,7 +107,7 @@ class ImportaListiniAction ->mapWithKeys(fn (array $a) => [$a['codice'] => $a['modello_id'] ?? null]) ->all(); - $aggiornati = ListinoImporter::importa($record, $blocchi, $associazioni); + $aggiornati = ListinoImporter::importa($record, $blocchi, $associazioni, preservaPersonalizzati: ! ($data['sovrascrivi_tutto'] ?? true)); Notification::make() ->title('Listini importati') diff --git a/backoffice/app/Models/ListinoPrezzo.php b/backoffice/app/Models/ListinoPrezzo.php index a8a3271..c5d49cc 100644 --- a/backoffice/app/Models/ListinoPrezzo.php +++ b/backoffice/app/Models/ListinoPrezzo.php @@ -10,13 +10,14 @@ class ListinoPrezzo extends Model protected $table = 'listino_prezzi'; protected $fillable = [ - 'modello_id', 'larghezza', 'altezza', 'prezzo', + 'modello_id', 'larghezza', 'altezza', 'prezzo', 'personalizzato', ]; protected $casts = [ 'larghezza' => 'integer', 'altezza' => 'integer', 'prezzo' => 'float', + 'personalizzato' => 'boolean', ]; public function modello(): BelongsTo diff --git a/backoffice/app/Support/Listini/ListinoImporter.php b/backoffice/app/Support/Listini/ListinoImporter.php index 212c9b2..99cd2ab 100644 --- a/backoffice/app/Support/Listini/ListinoImporter.php +++ b/backoffice/app/Support/Listini/ListinoImporter.php @@ -46,17 +46,19 @@ class ListinoImporter /** * Sostituisce il listino di ogni modello associato con i prezzi del rispettivo blocco. + * Con $preservaPersonalizzati i prezzi inseriti a mano restano e prevalgono sull'Excel + * per la stessa misura; altrimenti l'Excel sostituisce tutto. * * @param list}> $blocchi * @param array $associazioni codice del file → id modello * @return int numero di modelli aggiornati */ - public static function importa(Sistema $sistema, array $blocchi, array $associazioni): int + public static function importa(Sistema $sistema, array $blocchi, array $associazioni, bool $preservaPersonalizzati = false): int { $prezziPerCodice = collect($blocchi)->keyBy('codice')->map(fn (array $b) => $b['prezzi']); $modelli = $sistema->modelli()->get()->keyBy('id'); - return DB::transaction(function () use ($associazioni, $prezziPerCodice, $modelli) { + return DB::transaction(function () use ($associazioni, $prezziPerCodice, $modelli, $preservaPersonalizzati) { $aggiornati = 0; $ora = now(); @@ -66,10 +68,22 @@ class ListinoImporter continue; } - $modello->listino()->delete(); + $misura = fn ($p) => (int) $p['larghezza'].'x'.(int) $p['altezza']; + + $daEliminare = $modello->listino(); + $personalizzate = []; + if ($preservaPersonalizzati) { + $daEliminare->where('personalizzato', false); + $personalizzate = $modello->listino()->where('personalizzato', true)->get() + ->mapWithKeys(fn ($p) => [$misura($p) => true]) + ->all(); + } + $daEliminare->delete(); + // Una stessa misura ripetuta nel blocco: vale l'ultima. $righe = collect($prezziPerCodice[$codice]) - ->keyBy(fn (array $p) => $p['larghezza'].'x'.$p['altezza']) + ->keyBy($misura) + ->reject(fn (array $p, string $chiave) => isset($personalizzate[$chiave])) ->map(fn (array $p) => $p + ['modello_id' => $modello->id, 'created_at' => $ora, 'updated_at' => $ora]) ->values(); foreach ($righe->chunk(500) as $chunk) { diff --git a/backoffice/database/migrations/2026_10_19_160000_add_personalizzato_to_listino_prezzi_table.php b/backoffice/database/migrations/2026_10_19_160000_add_personalizzato_to_listino_prezzi_table.php new file mode 100644 index 0000000..39771dd --- /dev/null +++ b/backoffice/database/migrations/2026_10_19_160000_add_personalizzato_to_listino_prezzi_table.php @@ -0,0 +1,23 @@ +boolean('personalizzato')->default(false)->after('prezzo'); + }); + } + + public function down(): void + { + Schema::table('listino_prezzi', function (Blueprint $table) { + $table->dropColumn('personalizzato'); + }); + } +}; diff --git a/backoffice/tests/Feature/ImportazioneListiniTest.php b/backoffice/tests/Feature/ImportazioneListiniTest.php index 189cb4c..daa4c6b 100644 --- a/backoffice/tests/Feature/ImportazioneListiniTest.php +++ b/backoffice/tests/Feature/ImportazioneListiniTest.php @@ -127,6 +127,61 @@ class ImportazioneListiniTest extends TestCase $this->assertNotNull($finestra->fresh()->listino_aggiornato_at); } + /** Listino con un prezzo da Excel e due aggiunti a mano (uno su una misura presente nel file). */ + private function finestraConPrezziPersonalizzati(): Modello + { + $finestra = Modello::where('codice', 'finestra_1_anta')->firstOrFail(); + ListinoPrezzo::create(['modello_id' => $finestra->id, 'larghezza' => 400, 'altezza' => 400, 'prezzo' => 1]); + ListinoPrezzo::create(['modello_id' => $finestra->id, 'larghezza' => 500, 'altezza' => 400, 'prezzo' => 999, 'personalizzato' => true]); + ListinoPrezzo::create(['modello_id' => $finestra->id, 'larghezza' => 2000, 'altezza' => 2000, 'prezzo' => 888, 'personalizzato' => true]); + + return $finestra; + } + + private function importaFinestra(Modello $finestra, ?bool $sovrascriviTutto): void + { + $data = [ + 'file' => $this->fileExcel(), + 'associazioni' => ['a' => ['codice' => 'finestra_1_anta', 'modello_id' => $finestra->id]], + ]; + if ($sovrascriviTutto !== null) { + $data['sovrascrivi_tutto'] = $sovrascriviTutto; + } + + Livewire::test(ListSistemi::class) + ->callTableAction('importaListini', $this->pvc, data: $data) + ->assertHasNoTableActionErrors(); + } + + public function test_di_default_l_excel_sovrascrive_anche_i_prezzi_personalizzati(): void + { + $finestra = $this->finestraConPrezziPersonalizzati(); + + Livewire::test(ListSistemi::class) + ->mountTableAction('importaListini', $this->pvc) + ->assertTableActionDataSet(['sovrascrivi_tutto' => true]); + + $this->importaFinestra($finestra, null); + + $this->assertSame(3, $finestra->listino()->count()); + $this->assertFalse($finestra->listino()->where('personalizzato', true)->exists()); + $this->assertEquals(381.24, $finestra->listino()->where('larghezza', 500)->where('altezza', 400)->value('prezzo')); + } + + public function test_si_possono_preservare_i_prezzi_personalizzati(): void + { + $finestra = $this->finestraConPrezziPersonalizzati(); + + $this->importaFinestra($finestra, false); + + // 3 misure dal file (una sostituita dal prezzo personalizzato) + 1 personalizzata fuori file. + $this->assertSame(4, $finestra->listino()->count()); + $this->assertEquals(367.2, $finestra->listino()->where('larghezza', 400)->where('altezza', 400)->value('prezzo')); + $this->assertEquals(999, $finestra->listino()->where('larghezza', 500)->where('altezza', 400)->value('prezzo')); + $this->assertEquals(888, $finestra->listino()->where('larghezza', 2000)->value('prezzo')); + $this->assertSame(2, $finestra->listino()->where('personalizzato', true)->count()); + } + public function test_due_tabelle_non_possono_andare_sullo_stesso_modello(): void { $finestra = Modello::where('codice', 'finestra_1_anta')->firstOrFail(); @@ -157,6 +212,8 @@ class ImportazioneListiniTest extends TestCase ->assertHasTableActionErrors(['larghezza' => 'unique']); $this->assertSame(2, $finestra->listino()->count()); + $this->assertTrue($finestra->listino()->where('larghezza', 500)->value('personalizzato')); + $this->assertFalse($prezzo->fresh()->personalizzato); } public function test_api_listino_restituisce_la_griglia_ordinata(): void