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:
2026-10-19 16:51:24 +02:00
parent a8aa6d9927
commit 1e352eb2ef
46 changed files with 1575 additions and 327 deletions
@@ -0,0 +1,36 @@
<?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
{
// Griglia prezzi del modello: un prezzo per ogni coppia larghezza × altezza.
Schema::create('listino_prezzi', function (Blueprint $table) {
$table->id();
$table->foreignId('modello_id')->constrained('modelli')->cascadeOnDelete();
$table->decimal('larghezza', 8, 2);
$table->decimal('altezza', 8, 2);
$table->decimal('prezzo', 10, 2);
$table->timestamps();
$table->unique(['modello_id', 'larghezza', 'altezza']);
});
Schema::table('modelli', function (Blueprint $table) {
$table->timestamp('listino_aggiornato_at')->nullable()->after('moduli');
});
}
public function down(): void
{
Schema::table('modelli', function (Blueprint $table) {
$table->dropColumn('listino_aggiornato_at');
});
Schema::dropIfExists('listino_prezzi');
}
};
@@ -0,0 +1,65 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Le misure passano da centimetri a millimetri: listini dei modelli e misure dei
* prodotti già nei preventivi (colonne e payload del configuratore).
*/
return new class extends Migration
{
public function up(): void
{
$this->converti(10);
Schema::table('listino_prezzi', function (Blueprint $table) {
$table->unsignedInteger('larghezza')->change();
$table->unsignedInteger('altezza')->change();
});
}
public function down(): void
{
Schema::table('listino_prezzi', function (Blueprint $table) {
$table->decimal('larghezza', 8, 2)->change();
$table->decimal('altezza', 8, 2)->change();
});
$this->converti(0.1);
}
private function converti(float $fattore): void
{
// In due passaggi (prima negativi, poi positivi): MySQL verifica l'indice univoco
// riga per riga, e un valore già convertito (30 → 300) collide con uno non ancora
// convertito. I negativi non collidono con nessun valore originale.
DB::table('listino_prezzi')->update([
'larghezza' => DB::raw("-ROUND(larghezza * {$fattore}, 2)"),
'altezza' => DB::raw("-ROUND(altezza * {$fattore}, 2)"),
]);
DB::table('listino_prezzi')->update([
'larghezza' => DB::raw('-larghezza'),
'altezza' => DB::raw('-altezza'),
]);
DB::table('preventivo_items')->orderBy('id')->chunkById(200, function ($items) use ($fattore) {
foreach ($items as $item) {
$payload = json_decode($item->payload ?? 'null', true);
foreach (['width', 'height'] as $chiave) {
if (is_array($payload) && is_numeric($payload[$chiave] ?? null)) {
$payload[$chiave] = (string) round((float) $payload[$chiave] * $fattore, 2);
}
}
DB::table('preventivo_items')->where('id', $item->id)->update([
'larghezza' => $item->larghezza === null ? null : (int) round($item->larghezza * $fattore),
'altezza' => $item->altezza === null ? null : (int) round($item->altezza * $fattore),
'payload' => $payload === null ? $item->payload : json_encode($payload),
]);
}
});
}
};