feat: Aggiungi supporto per prezzi personalizzati nel listino e modifica la logica di importazione
This commit is contained in:
+8
-1
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<array{codice: string, prezzi: list<array>}> $blocchi
|
||||
* @param array<string, int|null> $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) {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('listino_prezzi', function (Blueprint $table) {
|
||||
// Prezzo aggiunto a mano dal pannello (pulsante "Nuovo"), non dall'importazione Excel.
|
||||
$table->boolean('personalizzato')->default(false)->after('prezzo');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('listino_prezzi', function (Blueprint $table) {
|
||||
$table->dropColumn('personalizzato');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user