feat: add previous configuration handling for user selections in quotes
- Implemented session storage management for the last configuration choices made by the user. - Added functionality to prompt users when changing models to retain their previous selections. - Introduced methods to apply previous choices to the newly loaded model, including finishes, handles, glass types, and options. - Enhanced user experience by ensuring selections are automatically applied where applicable.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Configuratore;
|
||||
|
||||
use App\Models\Finitura;
|
||||
use App\Models\GruppoOpzioni;
|
||||
use App\Models\Modello;
|
||||
use App\Models\Opzione;
|
||||
use App\Models\Vetro;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Prezzo di una configurazione, calcolato solo lato server dalle regole del backoffice:
|
||||
*
|
||||
* prezzo = base x (1 + supplementi % / 100) + supplementi fissi + variazione vetro
|
||||
*
|
||||
* - base: prima misura del listino >= a quella richiesta (mm), entro min/max del listino;
|
||||
* - supplementi: tipo finitura (bianco/uniforme/bicolore) e opzioni scelte (gruppi di Opzione);
|
||||
* - variazione vetro: somma delle aree fatturabili delle lastre (max(area, minimo) per lastra)
|
||||
* per la maggiorazione €/m² del riempimento, con le lastre date dalla regola vetro del modello.
|
||||
*
|
||||
* I calcoli interni non sono arrotondati: solo il prezzo finale va a 2 decimali.
|
||||
*/
|
||||
class CalcolatorePrezzo
|
||||
{
|
||||
/**
|
||||
* @param array{larghezza?:mixed,altezza?:mixed,finitura_interna?:mixed,finitura_esterna?:mixed,maniglia?:mixed,vetro?:mixed,opzioni?:array<string,mixed>} $scelte
|
||||
* @return array{prezzo:float,scelte:array,configurazione:array,dettaglio:array}
|
||||
*
|
||||
* @throws ValidationException se la misura è fuori listino o una scelta non esiste
|
||||
*/
|
||||
public function calcola(Modello $modello, array $scelte): array
|
||||
{
|
||||
$larghezza = (int) ($scelte['larghezza'] ?? 0);
|
||||
$altezza = (int) ($scelte['altezza'] ?? 0);
|
||||
[$base, $misuraListino] = $this->prezzoBase($modello, $larghezza, $altezza);
|
||||
|
||||
$opzioni = Opzione::perModello($modello);
|
||||
$supplementi = [];
|
||||
$configurazione = [
|
||||
'nome' => $modello->titolo ?: ($modello->descrizione ?: $modello->nome),
|
||||
'width' => $larghezza,
|
||||
'height' => $altezza,
|
||||
];
|
||||
$normalizzate = ['larghezza' => $larghezza, 'altezza' => $altezza, 'opzioni' => []];
|
||||
|
||||
if ($modello->haModulo('modello')) {
|
||||
// Finiture attive nel modello, ciascuna scelta solo tra quelle del suo lato.
|
||||
$profili = Finitura::delProfiloPerModello($modello);
|
||||
$interna = $this->scegli($profili->filter->disponibileSu('interno'), $scelte['finitura_interna'] ?? null, 'finitura_interna', fn ($f) => $f->predefinita);
|
||||
// Senza finitura esterna separata il serramento è tutto della finitura interna.
|
||||
$esterna = $modello->haModulo('finitura_esterna')
|
||||
? $this->scegli($profili->filter->disponibileSu('esterno'), $scelte['finitura_esterna'] ?? null, 'finitura_esterna', fn ($f) => $f->predefinita)
|
||||
: $interna;
|
||||
|
||||
$tipo = match (true) {
|
||||
$interna->isNot($esterna) => 'bicolore',
|
||||
$interna->predefinita => 'bianco',
|
||||
default => 'uniforme',
|
||||
};
|
||||
$voce = $opzioni->get('tipo_finitura', collect())->firstWhere('valore', $tipo);
|
||||
$supplementi[] = $this->supplemento('Tipo finitura', $voce, $base, $larghezza, $altezza, $tipo);
|
||||
|
||||
$configurazione['tipo_finitura'] = $voce?->etichetta ?? ucfirst($tipo);
|
||||
$configurazione['finitura_interna'] = $interna->descrizione;
|
||||
if ($modello->haModulo('finitura_esterna')) {
|
||||
$configurazione['finitura_esterna'] = $esterna->descrizione;
|
||||
}
|
||||
$normalizzate['finitura_interna'] = $interna->codice;
|
||||
$normalizzate['finitura_esterna'] = $esterna->codice;
|
||||
}
|
||||
|
||||
if ($modello->haModulo('maniglia')) {
|
||||
$maniglie = $modello->sistema->finiture()->where('tipo', 'maniglia')->attive()->get();
|
||||
if ($maniglie->isNotEmpty()) {
|
||||
$maniglia = $this->scegli($maniglie, $scelte['maniglia'] ?? null, 'maniglia');
|
||||
$configurazione['maniglia'] = $maniglia->descrizione;
|
||||
$normalizzate['maniglia'] = $maniglia->codice;
|
||||
}
|
||||
}
|
||||
|
||||
$gruppi = GruppoOpzioni::perCodice();
|
||||
foreach ($opzioni as $gruppo => $righe) {
|
||||
$definizione = $gruppi->get($gruppo);
|
||||
if (! $definizione || ! $definizione->haControllo() || ! $modello->haModulo($definizione->modulo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($definizione->ui === 'multipla') {
|
||||
$scelteGruppo = $this->scegliMultiple($righe, $scelte['opzioni'][$gruppo] ?? [], "opzioni.{$gruppo}");
|
||||
foreach ($scelteGruppo as $scelta) {
|
||||
$supplementi[] = $this->supplemento($definizione->etichetta, $scelta, $base, $larghezza, $altezza);
|
||||
}
|
||||
if ($scelteGruppo->isNotEmpty()) {
|
||||
$configurazione[$gruppo] = $scelteGruppo->pluck('etichetta')->implode(', ');
|
||||
}
|
||||
$normalizzate['opzioni'][$gruppo] = $scelteGruppo->pluck('valore')->all();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$scelta = $this->scegli($righe, $scelte['opzioni'][$gruppo] ?? null, "opzioni.{$gruppo}", null, 'valore');
|
||||
$supplementi[] = $this->supplemento($definizione->etichetta, $scelta, $base, $larghezza, $altezza);
|
||||
$configurazione[$gruppo] = $scelta->etichetta;
|
||||
$normalizzate['opzioni'][$gruppo] = $scelta->valore;
|
||||
}
|
||||
|
||||
$vetro = null;
|
||||
if ($modello->haModulo('vetro')) {
|
||||
$vetri = $modello->sistema->vetri()->attivi()->get();
|
||||
if ($vetri->isNotEmpty()) {
|
||||
$scelto = $this->scegli($vetri, $scelte['vetro'] ?? null, 'vetro', fn ($v) => $v->predefinito);
|
||||
$vetro = $this->variazioneVetro($modello, $scelto, $larghezza, $altezza);
|
||||
$configurazione['vetro'] = $scelto->descrizione;
|
||||
$normalizzate['vetro'] = $scelto->codice;
|
||||
}
|
||||
}
|
||||
|
||||
$supplementi = array_values(array_filter($supplementi));
|
||||
$percentuale = array_sum(array_column(array_filter($supplementi, fn ($s) => $s['tipo'] === 'percentuale'), 'valore'));
|
||||
// Importi fissi e al metro lineare (già calcolati sulle misure).
|
||||
$fisso = array_sum(array_column(array_filter($supplementi, fn ($s) => $s['tipo'] !== 'percentuale'), 'importo'));
|
||||
$prezzo = $base * (1 + $percentuale / 100) + $fisso + ($vetro['importo'] ?? 0);
|
||||
|
||||
return [
|
||||
'prezzo' => round($prezzo, 2),
|
||||
'scelte' => $normalizzate,
|
||||
'configurazione' => $configurazione,
|
||||
'dettaglio' => [
|
||||
'prezzo_base' => $base,
|
||||
'misura_listino' => $misuraListino,
|
||||
'supplementi' => $supplementi,
|
||||
'percentuale_totale' => $percentuale,
|
||||
'fisso_totale' => $fisso,
|
||||
'vetro' => $vetro,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{0:float,1:array{larghezza:int,altezza:int}} */
|
||||
private function prezzoBase(Modello $modello, int $larghezza, int $altezza): array
|
||||
{
|
||||
$griglia = $modello->listino()->get(['larghezza', 'altezza', 'prezzo']);
|
||||
if ($griglia->isEmpty()) {
|
||||
throw ValidationException::withMessages(['larghezza' => 'Nessun listino caricato per questo modello.']);
|
||||
}
|
||||
|
||||
$l = $this->misuraListino($griglia->pluck('larghezza'), $larghezza);
|
||||
$h = $this->misuraListino($griglia->pluck('altezza'), $altezza);
|
||||
$cella = ($l !== null && $h !== null)
|
||||
? $griglia->first(fn ($p) => $p->larghezza === $l && $p->altezza === $h)
|
||||
: null;
|
||||
|
||||
if (! $cella) {
|
||||
throw ValidationException::withMessages(['larghezza' => 'Misura non disponibile a listino.']);
|
||||
}
|
||||
|
||||
return [(float) $cella->prezzo, ['larghezza' => $l, 'altezza' => $h]];
|
||||
}
|
||||
|
||||
/** Prima misura del listino >= a quella richiesta; null se fuori dal min/max del listino. */
|
||||
private function misuraListino(Collection $misure, int $valore): ?int
|
||||
{
|
||||
$misure = $misure->unique()->sort()->values();
|
||||
|
||||
if ($valore <= 0 || $valore < $misure->first()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $misure->first(fn ($m) => $m >= $valore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Elemento scelto per codice (o valore); senza scelta il predefinito, altrimenti il primo.
|
||||
*
|
||||
* @template T of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param Collection<int,T> $elementi
|
||||
* @return T
|
||||
*/
|
||||
private function scegli(Collection $elementi, mixed $codice, string $campo, ?callable $predefinito = null, string $chiave = 'codice')
|
||||
{
|
||||
if ($codice === null || $codice === '') {
|
||||
$scelto = ($predefinito ? $elementi->first($predefinito) : null) ?? $elementi->first();
|
||||
} else {
|
||||
$scelto = $elementi->first(fn ($e) => (string) $e->{$chiave} === (string) $codice);
|
||||
}
|
||||
|
||||
if (! $scelto) {
|
||||
throw ValidationException::withMessages([$campo => 'Scelta non disponibile per questo modello.']);
|
||||
}
|
||||
|
||||
return $scelto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opzioni scelte in un gruppo a scelta multipla (anche nessuna); quelle con la stessa
|
||||
* `alternativa` si escludono a vicenda.
|
||||
*
|
||||
* @return Collection<int,Opzione>
|
||||
*/
|
||||
private function scegliMultiple(Collection $righe, mixed $valori, string $campo): Collection
|
||||
{
|
||||
$valori = collect(is_array($valori) ? $valori : [$valori])->filter(fn ($v) => is_scalar($v) && $v !== '')->map(fn ($v) => (string) $v)->unique();
|
||||
|
||||
$scelte = $valori->map(fn (string $v) => $this->scegli($righe, $v, $campo, null, 'valore'));
|
||||
|
||||
$doppie = $scelte->whereNotNull('alternativa')->groupBy('alternativa')->filter(fn ($g) => $g->count() > 1);
|
||||
if ($doppie->isNotEmpty()) {
|
||||
throw ValidationException::withMessages([$campo => 'Si può scegliere una sola opzione tra: '.$doppie->first()->pluck('etichetta')->implode(', ').'.']);
|
||||
}
|
||||
|
||||
// Nell'ordine del gruppo, non in quello di invio.
|
||||
return $righe->filter(fn (Opzione $o) => $scelte->contains(fn (Opzione $s) => $s->is($o)))->values();
|
||||
}
|
||||
|
||||
private function supplemento(string $voce, ?Opzione $opzione, float $base, int $larghezza, int $altezza, ?string $valore = null): ?array
|
||||
{
|
||||
if (! $opzione || ! $opzione->supplemento_tipo || ! $opzione->supplemento) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'voce' => "{$voce}: {$opzione->etichetta}",
|
||||
'valore_scelto' => $valore ?? $opzione->valore,
|
||||
'tipo' => $opzione->supplemento_tipo,
|
||||
'valore' => $opzione->supplemento,
|
||||
'metri_lineari' => $opzione->supplemento_tipo === 'metro_lineare' ? round($opzione->metriLineari($larghezza, $altezza), 3) : null,
|
||||
'importo' => $opzione->importo($base, $larghezza, $altezza),
|
||||
];
|
||||
}
|
||||
|
||||
private function variazioneVetro(Modello $modello, Vetro $vetro, int $larghezza, int $altezza): array
|
||||
{
|
||||
$regola = $modello->regolaVetro;
|
||||
$lastre = [];
|
||||
$mqFatturabili = 0.0;
|
||||
|
||||
foreach ($regola?->calcolaLastre($larghezza, $altezza) ?? [] as $lastra) {
|
||||
$mq = max(0, $lastra['larghezza']) * max(0, $lastra['altezza']) / 1_000_000;
|
||||
// Il minimo fatturabile vale per singola lastra.
|
||||
$fatturabili = max($mq, $vetro->minimo_mq);
|
||||
$mqFatturabili += $fatturabili * $lastra['quantita'];
|
||||
|
||||
$lastre[] = $lastra + ['mq' => round($mq, 3), 'mq_fatturabili' => round($fatturabili, 3)];
|
||||
}
|
||||
|
||||
return [
|
||||
'codice' => $vetro->codice,
|
||||
'descrizione' => $vetro->descrizione,
|
||||
'regola' => $regola?->codice,
|
||||
'maggiorazione_mq' => $vetro->maggiorazione_mq,
|
||||
'minimo_mq' => $vetro->minimo_mq,
|
||||
'lastre' => $lastre,
|
||||
'mq_fatturabili' => round($mqFatturabili, 3),
|
||||
'importo' => $mqFatturabili * $vetro->maggiorazione_mq,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user