feat: Implement price list management and import functionality
- Added ListinoRelationManager for managing price lists associated with models. - Introduced ImportaListiniAction for importing price lists from Excel files. - Created ListinoPrezzo model to represent price entries in the database. - Developed ListinoExcelParser for parsing Excel files containing price lists. - Implemented ListinoImporter to handle the import logic and associations. - Added migration for creating listino_prezzi table and converting measures to mm. - Updated frontend logic to handle new price list features and validations. - Enhanced tests for price list import and conversion functionalities.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ModelloResource\Pages;
|
||||
use App\Filament\Resources\ModelloResource\RelationManagers\ListinoRelationManager;
|
||||
use App\Models\Modello;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
@@ -126,6 +127,13 @@ class ModelloResource extends Resource
|
||||
->badge()
|
||||
->formatStateUsing(fn (?string $state) => Modello::CATEGORIE[$state] ?? $state)
|
||||
->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('listino_count')
|
||||
->label('Listino')
|
||||
->counts('listino')
|
||||
->formatStateUsing(fn (int $state) => $state ? "{$state} prezzi" : 'Nessuno')
|
||||
->color(fn (int $state) => $state ? null : 'gray')
|
||||
->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->file_modello)),
|
||||
@@ -146,6 +154,13 @@ class ModelloResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
ListinoRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModelloResource\RelationManagers;
|
||||
|
||||
use App\Models\ListinoPrezzo;
|
||||
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\Database\Eloquent\Builder;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
class ListinoRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'listino';
|
||||
|
||||
protected static ?string $title = 'Listino';
|
||||
|
||||
protected static ?string $modelLabel = 'prezzo';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'prezzi';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form->columns(3)->schema([
|
||||
Forms\Components\TextInput::make('larghezza')
|
||||
->label('Larghezza (mm)')
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->required()
|
||||
->unique(
|
||||
ignoreRecord: true,
|
||||
modifyRuleUsing: fn (Unique $rule, Forms\Get $get) => $rule
|
||||
->where('modello_id', $this->getOwnerRecord()->getKey())
|
||||
->where('altezza', $get('altezza')),
|
||||
)
|
||||
->validationMessages(['unique' => 'Esiste già un prezzo per questa misura.']),
|
||||
Forms\Components\TextInput::make('altezza')
|
||||
->label('Altezza (mm)')
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('prezzo')
|
||||
->label('Prezzo')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->prefix('€')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$misure = fn (string $colonna) => fn () => ListinoPrezzo::query()
|
||||
->where('modello_id', $this->getOwnerRecord()->getKey())
|
||||
->orderBy($colonna)
|
||||
->distinct()
|
||||
->pluck($colonna)
|
||||
->mapWithKeys(fn ($v) => [$v => $this->mm($v)])
|
||||
->all();
|
||||
|
||||
return $table
|
||||
->description(fn () => ($data = $this->getOwnerRecord()->listino_aggiornato_at)
|
||||
? 'Ultimo aggiornamento: '.$data->format('d/m/Y H:i').'. Il configuratore usa la prima misura del listino uguale o superiore a quella richiesta.'
|
||||
: 'Nessun listino caricato: importalo dalla lista Sistemi o aggiungi i prezzi a mano.')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->orderBy('altezza')->orderBy('larghezza'))
|
||||
->defaultPaginationPageOption(25)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('altezza')
|
||||
->label('Altezza')
|
||||
->formatStateUsing(fn ($state) => $this->mm($state))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('larghezza')
|
||||
->label('Larghezza')
|
||||
->formatStateUsing(fn ($state) => $this->mm($state))
|
||||
->sortable(),
|
||||
Tables\Columns\TextInputColumn::make('prezzo')
|
||||
->label('Prezzo (€)')
|
||||
->type('number')
|
||||
->step('0.01')
|
||||
->rules(['required', 'numeric', 'min:0'])
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('altezza')->label('Altezza')->options($misure('altezza')),
|
||||
Tables\Filters\SelectFilter::make('larghezza')->label('Larghezza')->options($misure('larghezza')),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make(),
|
||||
Tables\Actions\Action::make('svuota')
|
||||
->label('Svuota listino')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalDescription('Verranno eliminati tutti i prezzi del modello. Il configuratore non potrà calcolare il prezzo finché non sarà caricato un nuovo listino.')
|
||||
->visible(fn () => $this->getOwnerRecord()->listino()->exists())
|
||||
->action(function () {
|
||||
$modello = $this->getOwnerRecord();
|
||||
$modello->listino()->delete();
|
||||
$modello->update(['listino_aggiornato_at' => null]);
|
||||
|
||||
Notification::make()->title('Listino svuotato')->success()->send();
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isReadOnly(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function mm(mixed $valore): string
|
||||
{
|
||||
return number_format((int) $valore, 0, ',', '.').' mm';
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -32,7 +32,7 @@ class ItemsRelationManager extends RelationManager
|
||||
Tables\Columns\TextColumn::make('dimensioni')
|
||||
->label('Dimensioni')
|
||||
->state(fn (Model $record) => $record->larghezza && $record->altezza
|
||||
? "{$record->larghezza} × {$record->altezza} cm" : '—'),
|
||||
? "{$record->larghezza} × {$record->altezza} mm" : '—'),
|
||||
Tables\Columns\TextColumn::make('quantita')->label('Q.tà'),
|
||||
Tables\Columns\TextColumn::make('prezzo_unitario')->label('Prezzo un.')->money('EUR'),
|
||||
Tables\Columns\TextColumn::make('prezzo_totale')->label('Totale')->money('EUR'),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\SistemaResource\Actions\ImportaListiniAction;
|
||||
use App\Filament\Resources\SistemaResource\Pages;
|
||||
use App\Models\Sistema;
|
||||
use Filament\Forms;
|
||||
@@ -77,6 +78,7 @@ class SistemaResource extends Resource
|
||||
->url(fn (Sistema $record) => ModelloResource::getUrl('index', [
|
||||
'tableFilters' => ['sistema' => ['value' => $record->getKey()]],
|
||||
])),
|
||||
ImportaListiniAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->modalDescription('Verrà eliminato definitivamente il sistema insieme a tutti i suoi modelli.'),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\SistemaResource\Actions;
|
||||
|
||||
use App\Models\Sistema;
|
||||
use App\Support\Listini\ListinoExcelParser;
|
||||
use App\Support\Listini\ListinoImporter;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Importa i listini di un sistema da Excel in due passi: caricamento del file e
|
||||
* revisione delle associazioni tabella del file → modello prima di salvare.
|
||||
*/
|
||||
class ImportaListiniAction
|
||||
{
|
||||
public static function make(): Action
|
||||
{
|
||||
return Action::make('importaListini')
|
||||
->label('Importa listini')
|
||||
->icon('heroicon-o-arrow-up-tray')
|
||||
->color('gray')
|
||||
->modalHeading(fn (Sistema $record) => "Importa listini {$record->nome}")
|
||||
->modalWidth('3xl')
|
||||
->modalSubmitActionLabel('Importa listini')
|
||||
->steps(fn (Sistema $record) => [
|
||||
Forms\Components\Wizard\Step::make('File')
|
||||
->description('Carica il file Excel')
|
||||
->schema([
|
||||
Forms\Components\FileUpload::make('file')
|
||||
->label('File listini')
|
||||
->helperText('File .xls o .xlsx: ogni tabella ha nella prima colonna il codice dell\'infisso, nella prima riga le larghezze e nella prima colonna delle righe successive le altezze (in mm).')
|
||||
->required()
|
||||
->storeFiles(false)
|
||||
->maxSize(10240),
|
||||
])
|
||||
->afterValidation(function (Get $get, Set $set) use ($record) {
|
||||
$blocchi = self::leggi($get('file'));
|
||||
|
||||
$associazioni = ListinoImporter::suggerisciAssociazioni($record, array_column($blocchi, 'codice'));
|
||||
|
||||
$set('associazioni', collect($blocchi)->mapWithKeys(fn (array $b) => [(string) Str::uuid() => [
|
||||
'codice' => $b['codice'],
|
||||
'prezzi' => count($b['prezzi']),
|
||||
'modello_id' => $associazioni[$b['codice']],
|
||||
]])->all());
|
||||
}),
|
||||
Forms\Components\Wizard\Step::make('Associazioni')
|
||||
->description('Verifica i modelli associati')
|
||||
->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.'),
|
||||
// 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'])
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('h_codice')->hiddenLabel()->content('Codice nel file')->columnSpan(2),
|
||||
Forms\Components\Placeholder::make('h_prezzi')->hiddenLabel()->content('Prezzi')->columnSpan(1),
|
||||
Forms\Components\Placeholder::make('h_modello')->hiddenLabel()->content('Modello')->columnSpan(2),
|
||||
]),
|
||||
Forms\Components\Repeater::make('associazioni')
|
||||
->hiddenLabel()
|
||||
->addable(false)
|
||||
->deletable(false)
|
||||
->reorderable(false)
|
||||
->columns(5)
|
||||
->extraAttributes(['class' => '-mt-4'])
|
||||
->schema([
|
||||
Forms\Components\Hidden::make('codice'),
|
||||
Forms\Components\Placeholder::make('codice_visibile')
|
||||
->hiddenLabel()
|
||||
->content(fn (Get $get) => $get('codice'))
|
||||
->extraAttributes(['class' => 'font-mono break-all'])
|
||||
->columnSpan(2),
|
||||
Forms\Components\Placeholder::make('prezzi')
|
||||
->hiddenLabel()
|
||||
->content(fn (Get $get) => $get('prezzi'))
|
||||
->columnSpan(1),
|
||||
Forms\Components\Select::make('modello_id')
|
||||
->hiddenLabel()
|
||||
->placeholder('— Non importare —')
|
||||
->options(fn () => $record->modelli()->orderBy('ordine')->pluck('nome', 'id'))
|
||||
->searchable()
|
||||
->distinct()
|
||||
->columnSpan(2),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, Sistema $record) {
|
||||
$blocchi = self::leggi($data['file']);
|
||||
$associazioni = collect($data['associazioni'] ?? [])
|
||||
->mapWithKeys(fn (array $a) => [$a['codice'] => $a['modello_id'] ?? null])
|
||||
->all();
|
||||
|
||||
$aggiornati = ListinoImporter::importa($record, $blocchi, $associazioni);
|
||||
|
||||
Notification::make()
|
||||
->title('Listini importati')
|
||||
->body("Aggiornati i listini di {$aggiornati} modelli.")
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{codice: string, prezzi: list<array>}>
|
||||
*/
|
||||
private static function leggi(mixed $file): array
|
||||
{
|
||||
if (is_array($file)) {
|
||||
$file = reset($file);
|
||||
}
|
||||
|
||||
try {
|
||||
$blocchi = $file instanceof TemporaryUploadedFile ? ListinoExcelParser::parse($file->getRealPath()) : [];
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
$blocchi = [];
|
||||
}
|
||||
|
||||
if (! $blocchi) {
|
||||
Notification::make()
|
||||
->title('Nessun listino trovato')
|
||||
->body('Il file non è un Excel valido o non contiene tabelle nel formato atteso.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt;
|
||||
}
|
||||
|
||||
return $blocchi;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,26 @@ class CatalogoController extends Controller
|
||||
return response()->json($this->sistema($sistema));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sistemi/{codice}/modelli/{modello}/listino
|
||||
* Griglia prezzi del modello (misure in mm), ordinata per altezza e larghezza.
|
||||
*/
|
||||
public function listino(string $codice, string $modello): JsonResponse
|
||||
{
|
||||
$modello = Modello::attivi()
|
||||
->where('codice', $modello)
|
||||
->whereHas('sistema', fn ($q) => $q->where('codice', $codice)->where('attivo', true))
|
||||
->firstOrFail();
|
||||
|
||||
$prezzi = $modello->listino()
|
||||
->orderBy('altezza')
|
||||
->orderBy('larghezza')
|
||||
->get(['altezza', 'larghezza', 'prezzo'])
|
||||
->map(fn ($p) => ['altezza' => $p->altezza, 'larghezza' => $p->larghezza, 'prezzo' => $p->prezzo]);
|
||||
|
||||
return response()->json($prezzi);
|
||||
}
|
||||
|
||||
private function sistema(Sistema $sistema): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ListinoPrezzo extends Model
|
||||
{
|
||||
protected $table = 'listino_prezzi';
|
||||
|
||||
protected $fillable = [
|
||||
'modello_id', 'larghezza', 'altezza', 'prezzo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'larghezza' => 'integer',
|
||||
'altezza' => 'integer',
|
||||
'prezzo' => 'float',
|
||||
];
|
||||
|
||||
public function modello(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Modello::class);
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Le modifiche manuali dal pannello aggiornano la data del listino del modello.
|
||||
$tocca = fn (ListinoPrezzo $prezzo) => $prezzo->modello?->update(['listino_aggiornato_at' => now()]);
|
||||
|
||||
static::saved($tocca);
|
||||
static::deleted($tocca);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Modello extends Model
|
||||
@@ -36,12 +37,14 @@ class Modello extends Model
|
||||
protected $fillable = [
|
||||
'sistema_id', 'codice', 'nome', 'descrizione', 'icona', 'ordine', 'attivo',
|
||||
'cartella', 'file_modello', 'categoria', 'cartella_texture', 'moduli',
|
||||
'listino_aggiornato_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ordine' => 'integer',
|
||||
'attivo' => 'boolean',
|
||||
'moduli' => 'array',
|
||||
'listino_aggiornato_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function sistema(): BelongsTo
|
||||
@@ -49,6 +52,11 @@ class Modello extends Model
|
||||
return $this->belongsTo(Sistema::class);
|
||||
}
|
||||
|
||||
public function listino(): HasMany
|
||||
{
|
||||
return $this->hasMany(ListinoPrezzo::class);
|
||||
}
|
||||
|
||||
public function scopeAttivi(Builder $query): Builder
|
||||
{
|
||||
return $query->where('attivo', true)->orderBy('ordine')->orderBy('id');
|
||||
|
||||
@@ -13,8 +13,8 @@ class ConfigurazioneFormatter
|
||||
{
|
||||
/** Etichette italiane per le chiavi note. */
|
||||
private const LABELS = [
|
||||
'width' => 'Larghezza (cm)',
|
||||
'height' => 'Altezza (cm)',
|
||||
'width' => 'Larghezza (mm)',
|
||||
'height' => 'Altezza (mm)',
|
||||
'tipo_finitura' => 'Tipo finitura',
|
||||
'finitura_interna' => 'Finitura interna',
|
||||
'finitura_esterna' => 'Finitura esterna',
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Listini;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
|
||||
/**
|
||||
* Legge il file Excel dei listini (primo foglio). Il file contiene più tabelle una
|
||||
* sotto l'altra, ciascuna nella forma (misure in mm, come salvate a sistema):
|
||||
*
|
||||
* codice_infisso | 400 | 500 | ... ← larghezze in mm
|
||||
* 400 | p | p | ... ← altezza in mm + prezzi
|
||||
* ...
|
||||
*
|
||||
* Se la riga del titolo non contiene larghezze, l'intestazione è sulla riga successiva
|
||||
* (es. "CASSONETTO PVC" seguito da "mm | 500 | 600 ...").
|
||||
*/
|
||||
class ListinoExcelParser
|
||||
{
|
||||
/**
|
||||
* @return list<array{codice: string, prezzi: list<array{larghezza: int, altezza: int, prezzo: float}>}>
|
||||
*/
|
||||
public static function parse(string $path): array
|
||||
{
|
||||
$reader = IOFactory::createReader(IOFactory::identify($path));
|
||||
$reader->setReadDataOnly(true);
|
||||
|
||||
$righe = $reader->load($path)->getSheet(0)->toArray(null, true, false, false);
|
||||
|
||||
return self::parseRighe($righe);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<list<mixed>> $righe
|
||||
*/
|
||||
public static function parseRighe(array $righe): array
|
||||
{
|
||||
$blocchi = [];
|
||||
$n = count($righe);
|
||||
|
||||
for ($r = 0; $r < $n; $r++) {
|
||||
$titolo = $righe[$r][0] ?? null;
|
||||
if (! is_string($titolo) || trim($titolo) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$intestazione = $r;
|
||||
$larghezze = self::larghezze($righe[$r]);
|
||||
if (! $larghezze && isset($righe[$r + 1])) {
|
||||
$intestazione = $r + 1;
|
||||
$larghezze = self::larghezze($righe[$r + 1]);
|
||||
}
|
||||
if (! $larghezze) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prezzi = [];
|
||||
for ($rr = $intestazione + 1; $rr < $n; $rr++) {
|
||||
$altezza = $righe[$rr][0] ?? null;
|
||||
if (! is_numeric($altezza)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($larghezze as $colonna => $larghezza) {
|
||||
$prezzo = $righe[$rr][$colonna] ?? null;
|
||||
if (! is_numeric($prezzo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prezzi[] = [
|
||||
'larghezza' => (int) round($larghezza),
|
||||
'altezza' => (int) round((float) $altezza),
|
||||
'prezzo' => round((float) $prezzo, 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($prezzi) {
|
||||
$blocchi[] = ['codice' => trim($titolo), 'prezzi' => $prezzi];
|
||||
}
|
||||
|
||||
$r = $rr - 1;
|
||||
}
|
||||
|
||||
return $blocchi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Larghezze consecutive a partire dalla colonna B, indicizzate per colonna.
|
||||
*
|
||||
* @return array<int, float>
|
||||
*/
|
||||
private static function larghezze(array $riga): array
|
||||
{
|
||||
$larghezze = [];
|
||||
for ($c = 1; $c < count($riga) && is_numeric($riga[$c]); $c++) {
|
||||
$larghezze[$c] = (float) $riga[$c];
|
||||
}
|
||||
|
||||
return $larghezze;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Listini;
|
||||
|
||||
use App\Models\Modello;
|
||||
use App\Models\Sistema;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListinoImporter
|
||||
{
|
||||
// Codici del file con refusi noti rispetto al codice interno del modello.
|
||||
private const ALIAS = [
|
||||
'finestra_wasistas' => 'finestra_vasistas',
|
||||
];
|
||||
|
||||
/**
|
||||
* Associazione proposta codice-del-file → modello del sistema (null se nessun
|
||||
* modello corrisponde): per codice interno, poi per nome o descrizione.
|
||||
*
|
||||
* @param list<string> $codici
|
||||
* @return array<string, int|null>
|
||||
*/
|
||||
public static function suggerisciAssociazioni(Sistema $sistema, array $codici): array
|
||||
{
|
||||
$modelli = $sistema->modelli()->get();
|
||||
$associazioni = [];
|
||||
|
||||
foreach ($codici as $codice) {
|
||||
$associazioni[$codice] = self::trovaModello($modelli, $codice)?->id;
|
||||
}
|
||||
|
||||
return $associazioni;
|
||||
}
|
||||
|
||||
private static function trovaModello(Collection $modelli, string $codice): ?Modello
|
||||
{
|
||||
$slug = Str::slug($codice, '_');
|
||||
$slug = self::ALIAS[$slug] ?? $slug;
|
||||
|
||||
return $modelli->first(fn (Modello $m) => $m->codice === $slug)
|
||||
?? $modelli->first(fn (Modello $m) => Str::slug($m->nome, '_') === $slug)
|
||||
?? $modelli->first(fn (Modello $m) => $m->descrizione && Str::slug($m->descrizione, '_') === $slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sostituisce il listino di ogni modello associato con i prezzi del rispettivo blocco.
|
||||
*
|
||||
* @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
|
||||
{
|
||||
$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) {
|
||||
$aggiornati = 0;
|
||||
$ora = now();
|
||||
|
||||
foreach ($associazioni as $codice => $modelloId) {
|
||||
$modello = $modelloId ? $modelli->get($modelloId) : null;
|
||||
if (! $modello || ! $prezziPerCodice->has($codice)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modello->listino()->delete();
|
||||
// Una stessa misura ripetuta nel blocco: vale l'ultima.
|
||||
$righe = collect($prezziPerCodice[$codice])
|
||||
->keyBy(fn (array $p) => $p['larghezza'].'x'.$p['altezza'])
|
||||
->map(fn (array $p) => $p + ['modello_id' => $modello->id, 'created_at' => $ora, 'updated_at' => $ora])
|
||||
->values();
|
||||
foreach ($righe->chunk(500) as $chunk) {
|
||||
$modello->listino()->insert($chunk->all());
|
||||
}
|
||||
|
||||
$modello->update(['listino_aggiornato_at' => $ora]);
|
||||
$aggiornati++;
|
||||
}
|
||||
|
||||
return $aggiornati;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user