feat: remove obsolete glass configurations, add title import functionality

- Deleted the `vetro.json` file containing glass configurations.
- Introduced `ImportaTitoliAction` to facilitate importing model titles from Excel.
- Created `TitoliModelliImporter` for handling title import logic and updating existing models.
- Updated `SistemaResource` to include the new title import action.
- Modified `ConfigurazioneModello` to adjust the model name retrieval logic.
- Changed `ListinoImporter` to make the alias constant public.
- Added tests for title import functionality, ensuring correct updates and creation of new models.
This commit is contained in:
2026-10-23 11:15:28 +02:00
parent 139b1690ff
commit 0f0764367f
163 changed files with 284 additions and 19716 deletions
@@ -4,6 +4,7 @@ namespace App\Filament\Resources;
use App\Filament\Concerns\SoloSuperadmin;
use App\Filament\Resources\SistemaResource\Actions\ImportaListiniAction;
use App\Filament\Resources\SistemaResource\Actions\ImportaTitoliAction;
use App\Filament\Resources\SistemaResource\Pages;
use App\Models\Sistema;
use Filament\Forms;
@@ -83,6 +84,7 @@ class SistemaResource extends Resource
'tableFilters' => ['sistema' => ['value' => $record->getKey()]],
])),
ImportaListiniAction::make(),
ImportaTitoliAction::make(),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->modalDescription('Verrà eliminato definitivamente il sistema insieme a tutti i suoi modelli.'),
@@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\SistemaResource\Actions;
use App\Models\Sistema;
use App\Support\Listini\TitoliModelliImporter;
use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Throwable;
/**
* Importa da Excel i titoli dei modelli di un sistema (colonne ORDINE = codice e TITOLO):
* aggiorna solo il titolo dei modelli esistenti e crea disattivi quelli nuovi.
*/
class ImportaTitoliAction
{
public static function make(): Action
{
return Action::make('importaTitoli')
->label('Importa titoli')
->icon('heroicon-o-document-text')
->color('gray')
->modalHeading(fn (Sistema $record) => "Importa titoli dei modelli {$record->nome}")
->modalSubmitActionLabel('Importa titoli')
->form([
Forms\Components\FileUpload::make('file')
->label('File Excel')
->helperText('File .xls o .xlsx con le colonne ORDINE (codice del modello) e TITOLO. Dei modelli esistenti si aggiorna solo il titolo; i codici nuovi creano un modello disattivo.')
->required()
->storeFiles(false)
->maxSize(10240),
])
->action(function (array $data, Sistema $record) {
$file = is_array($data['file']) ? reset($data['file']) : $data['file'];
try {
$titoli = $file instanceof TemporaryUploadedFile ? TitoliModelliImporter::leggi($file->getRealPath()) : [];
} catch (Throwable $e) {
report($e);
$titoli = [];
}
if (! $titoli) {
Notification::make()
->title('Nessun titolo trovato')
->body('Il file non è un Excel valido o non ha le colonne ORDINE e TITOLO.')
->danger()
->send();
return;
}
$esito = TitoliModelliImporter::importa($record, $titoli);
$righe = [
count($esito['aggiornati']).' titoli aggiornati, '.$esito['invariati'].' già corretti.',
];
if ($esito['creati']) {
$righe[] = 'Nuovi modelli (disattivi): '.implode(', ', $esito['creati']).'.';
}
Notification::make()
->title('Titoli importati')
->body(implode(' ', $righe))
->success()
->persistent()
->send();
});
}
}
@@ -35,7 +35,7 @@ class ConfigurazioneModello
: collect();
return [
'nome' => $modello->titolo ?: ($modello->descrizione ?: $modello->nome),
'nome' => $modello->descrizione ?: ($modello->titolo ?: $modello->nome),
'form' => $this->form($modello, $interne->isNotEmpty(), $esterne->isNotEmpty(), $maniglie->isNotEmpty(), $vetri->isNotEmpty()),
'finiture' => $finiture->map($this->finitura(...))->values(),
'finiture_interne' => $interne,
@@ -99,7 +99,7 @@ class ConfigurazioneModello
}
return [
'nome' => $modello->titolo ?: ($modello->descrizione ?: $modello->nome),
'nome' => $modello->descrizione ?: ($modello->titolo ?: $modello->nome),
'radios' => (object) $radios,
'select' => (object) $select,
// Pulsanti selezionabili insieme (es. profili extra), anche nessuno.
@@ -11,7 +11,7 @@ use Illuminate\Support\Str;
class ListinoImporter
{
// Codici del file con refusi noti rispetto al codice interno del modello.
private const ALIAS = [
public const ALIAS = [
'finestra_wasistas' => 'finestra_vasistas',
];
@@ -0,0 +1,97 @@
<?php
namespace App\Support\Listini;
use App\Models\Modello;
use App\Models\Sistema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use PhpOffice\PhpSpreadsheet\IOFactory;
/**
* Importa i titoli dei modelli da Excel (es. reference/OrdineSerramenti_Descrizioni_NuoveOpzioni.xlsx):
* un foglio con intestazione ORDINE (codice del modello) e TITOLO. Se il codice esiste nel sistema
* si aggiorna solo il titolo; altrimenti si crea un modello disattivo con codice e titolo.
*/
class TitoliModelliImporter
{
/**
* Coppie codice → titolo dal primo foglio che ha le colonne ORDINE (o CODICE) e TITOLO.
*
* @return array<string, string>
*/
public static function leggi(string $path): array
{
foreach (IOFactory::load($path)->getAllSheets() as $foglio) {
$righe = $foglio->toArray(null, true, false, false);
foreach ($righe as $r => $riga) {
$intestazioni = array_map(fn ($v) => Str::upper(trim((string) $v)), $riga);
$colCodice = array_search('ORDINE', $intestazioni, true);
if ($colCodice === false) {
$colCodice = array_search('CODICE', $intestazioni, true);
}
$colTitolo = array_search('TITOLO', $intestazioni, true);
if ($colCodice === false || $colTitolo === false) {
continue;
}
$titoli = [];
foreach (array_slice($righe, $r + 1) as $dati) {
$codice = trim((string) ($dati[$colCodice] ?? ''));
$titolo = trim((string) ($dati[$colTitolo] ?? ''));
if ($codice !== '' && $titolo !== '') {
$titoli[$codice] = $titolo;
}
}
return $titoli;
}
}
return [];
}
/**
* @param array<string, string> $titoli codice → titolo
* @return array{aggiornati: list<string>, creati: list<string>, invariati: int}
*/
public static function importa(Sistema $sistema, array $titoli): array
{
$esito = ['aggiornati' => [], 'creati' => [], 'invariati' => 0];
DB::transaction(function () use ($sistema, $titoli, &$esito) {
$modelli = $sistema->modelli()->get()->keyBy('codice');
foreach ($titoli as $codice => $titolo) {
$codice = ListinoImporter::ALIAS[$codice] ?? $codice;
$modello = $modelli->get($codice);
if ($modello) {
if ($modello->titolo === $titolo) {
$esito['invariati']++;
continue;
}
$modello->update(['titolo' => $titolo]);
$esito['aggiornati'][] = $codice;
continue;
}
// Nuovo modello: solo codice e titolo, disattivo finché non viene configurato.
$modelli->put($codice, Modello::create([
'sistema_id' => $sistema->id,
'codice' => $codice,
'nome' => $titolo,
'titolo' => $titolo,
'attivo' => false,
'ordine' => $modelli->max('ordine') + 1,
]));
$esito['creati'][] = $codice;
}
});
return $esito;
}
}
@@ -0,0 +1,110 @@
<?php
namespace Tests\Feature;
use App\Filament\Resources\SistemaResource\Pages\ListSistemi;
use App\Models\Modello;
use App\Models\Sistema;
use App\Models\User;
use App\Support\Listini\TitoliModelliImporter;
use Database\Seeders\CatalogoSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\TestCase;
class ImportazioneTitoliTest extends TestCase
{
use RefreshDatabase;
private Sistema $pvc;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
$this->seed(CatalogoSeeder::class);
$this->pvc = Sistema::where('codice', 'pvc')->firstOrFail();
$this->actingAs(User::factory()->create(['is_superadmin' => true]));
}
/** Stesso formato del foglio "Nomi": intestazione, riga vuota, poi codice e titolo. */
private function fileExcel(): UploadedFile
{
$spreadsheet = new Spreadsheet;
$spreadsheet->getActiveSheet()->fromArray([
['ORDINE', 'TITOLO', 'CERNIERE A SCOMPARSA'],
[null, null, null],
['finestra_1_anta', 'FINESTRA 1 ANTA, A/R ', 'SI'],
['finestra_wasistas', 'WASISTAS CON MARTELLINA', 'SI'],
['modello_nuovo', 'MODELLO NUOVO', 'NO'],
['bilico', null, 'NO'],
], null, 'A1', true);
$path = tempnam(sys_get_temp_dir(), 'titoli');
(new Xlsx($spreadsheet))->save($path);
return UploadedFile::fake()->createWithContent('titoli.xlsx', file_get_contents($path));
}
public function test_legge_codici_e_titoli_dal_file_di_riferimento(): void
{
$titoli = TitoliModelliImporter::leggi(base_path('../reference/OrdineSerramenti_Descrizioni_NuoveOpzioni.xlsx'));
$this->assertCount(28, $titoli);
$this->assertSame('FINESTRA 2 ANTE, A/R', $titoli['finestra_2_ante']);
$this->assertSame('TELAIO FISSO', $titoli['finestra_anta_fissa']);
}
public function test_aggiorna_solo_il_titolo_dei_modelli_esistenti(): void
{
$finestra = Modello::where('sistema_id', $this->pvc->id)->where('codice', 'finestra_1_anta')->firstOrFail();
$prima = $finestra->only(['nome', 'descrizione', 'attivo', 'ordine', 'moduli']);
$esito = TitoliModelliImporter::importa($this->pvc, [
'finestra_1_anta' => 'FINESTRA 1 ANTA, A/R',
'finestra_wasistas' => 'WASISTAS CON MARTELLINA',
]);
$finestra->refresh();
$this->assertSame('FINESTRA 1 ANTA, A/R', $finestra->titolo);
$this->assertSame($prima, $finestra->only(['nome', 'descrizione', 'attivo', 'ordine', 'moduli']));
// Codice con refuso nel file: aggiorna il modello tramite alias, senza crearne uno nuovo.
$this->assertSame('WASISTAS CON MARTELLINA', Modello::where('codice', 'finestra_vasistas')->value('titolo'));
$this->assertFalse(Modello::where('codice', 'finestra_wasistas')->exists());
$this->assertSame(['finestra_1_anta', 'finestra_vasistas'], $esito['aggiornati']);
$this->assertSame([], $esito['creati']);
$this->assertSame(2, TitoliModelliImporter::importa($this->pvc, [
'finestra_1_anta' => 'FINESTRA 1 ANTA, A/R',
'finestra_wasistas' => 'WASISTAS CON MARTELLINA',
])['invariati']);
}
public function test_un_codice_nuovo_crea_un_modello_disattivo(): void
{
$esito = TitoliModelliImporter::importa($this->pvc, ['modello_nuovo' => 'MODELLO NUOVO']);
$nuovo = Modello::where('sistema_id', $this->pvc->id)->where('codice', 'modello_nuovo')->firstOrFail();
$this->assertSame('MODELLO NUOVO', $nuovo->titolo);
$this->assertFalse($nuovo->attivo);
$this->assertSame(['modello_nuovo'], $esito['creati']);
}
public function test_l_azione_importa_i_titoli_dal_pannello(): void
{
Livewire::test(ListSistemi::class)
->callTableAction('importaTitoli', $this->pvc, data: ['file' => $this->fileExcel()])
->assertHasNoTableActionErrors()
->assertNotified('Titoli importati');
$this->assertSame('FINESTRA 1 ANTA, A/R', Modello::where('codice', 'finestra_1_anta')->value('titolo'));
$this->assertSame('WASISTAS CON MARTELLINA', Modello::where('codice', 'finestra_vasistas')->value('titolo'));
$this->assertFalse(Modello::where('codice', 'modello_nuovo')->value('attivo'));
// Riga senza titolo: ignorata.
$this->assertNotSame('', Modello::where('codice', 'bilico')->value('titolo'));
}
}