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:
@@ -9,6 +9,7 @@ use App\Models\Cliente;
|
||||
use App\Models\Preventivo;
|
||||
use App\Models\PreventivoItem;
|
||||
use App\Support\ConfigurazioneFormatter;
|
||||
use App\Support\Configuratore\CalcolatorePrezzo;
|
||||
use App\Support\PreventivoPdf;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -21,39 +22,57 @@ use Illuminate\Support\Str;
|
||||
class CarrelloController extends Controller
|
||||
{
|
||||
/**
|
||||
* POST /api/carrello/aggiungi
|
||||
* Aggiunge un prodotto configurato al carrello (preventivo in stato bozza).
|
||||
* Voci descrittive del configuratore che non incidono sul prezzo: sono le uniche
|
||||
* accettate dal payload del client, tutto il resto lo ricostruisce il server.
|
||||
*/
|
||||
public function aggiungi(Request $request): JsonResponse
|
||||
private const VOCI_DESCRITTIVE = ['altezza_maniglia', 'apertura_portoncino', 'tipo_ispezione', 'avvolgibile', 'cassonetto'];
|
||||
|
||||
/**
|
||||
* POST /api/carrello/aggiungi
|
||||
* Aggiunge un prodotto configurato al carrello (preventivo in stato bozza). Il prezzo è
|
||||
* ricalcolato dalle scelte con le regole del backoffice: quello del client è ignorato.
|
||||
*/
|
||||
public function aggiungi(Request $request, CalcolatorePrezzo $calcolatore): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'token' => ['nullable', 'string', 'max:64'],
|
||||
'sistema' => ['nullable', 'string', 'max:50'],
|
||||
'type' => ['nullable', 'string', 'max:100'],
|
||||
'nome' => ['nullable', 'string', 'max:255'],
|
||||
'larghezza' => ['nullable', 'integer', 'min:0'],
|
||||
'altezza' => ['nullable', 'integer', 'min:0'],
|
||||
'sistema' => ['required', 'string', 'max:50'],
|
||||
'type' => ['required', 'string', 'max:100'],
|
||||
'qty' => ['nullable', 'integer', 'min:1', 'max:999'],
|
||||
'prezzo' => ['required', 'numeric', 'min:0'],
|
||||
'scelte' => ['required', 'array'],
|
||||
...CatalogoController::regoleScelte('scelte.'),
|
||||
'payload' => ['nullable', 'array'],
|
||||
'img' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$modello = CatalogoController::trovaModello($data['sistema'], $data['type']);
|
||||
$calcolo = $calcolatore->calcola($modello, $data['scelte']);
|
||||
|
||||
$descrittive = collect($data['payload'] ?? [])
|
||||
->only(self::VOCI_DESCRITTIVE)
|
||||
->filter(fn ($v) => is_scalar($v) && $v !== '')
|
||||
->map(fn ($v) => mb_substr((string) $v, 0, 255))
|
||||
->all();
|
||||
|
||||
$preventivo = $this->risolviCarrello($data['token'] ?? null);
|
||||
|
||||
$qty = $data['qty'] ?? 1;
|
||||
$prezzoUnitario = round((float) $data['prezzo'], 2);
|
||||
$prezzoUnitario = $calcolo['prezzo'];
|
||||
$configurazione = $calcolo['configurazione'];
|
||||
|
||||
$item = new PreventivoItem([
|
||||
'sistema' => $data['sistema'] ?? null,
|
||||
'type' => $data['type'] ?? null,
|
||||
'nome_modello' => $data['nome'] ?? ($data['payload']['nome'] ?? null),
|
||||
'larghezza' => $data['larghezza'] ?? ($data['payload']['width'] ?? null),
|
||||
'altezza' => $data['altezza'] ?? ($data['payload']['height'] ?? null),
|
||||
'sistema' => $modello->sistema->codice,
|
||||
'type' => $modello->codice,
|
||||
'nome_modello' => $configurazione['nome'],
|
||||
'larghezza' => $configurazione['width'],
|
||||
'altezza' => $configurazione['height'],
|
||||
'quantita' => $qty,
|
||||
'prezzo_unitario' => $prezzoUnitario,
|
||||
'prezzo_totale' => round($prezzoUnitario * $qty, 2),
|
||||
'payload' => $data['payload'] ?? [],
|
||||
'payload' => $descrittive + $configurazione + [
|
||||
'scelte' => $calcolo['scelte'],
|
||||
'dettaglio_prezzo' => $calcolo['dettaglio'],
|
||||
],
|
||||
'immagine_path' => $this->salvaImmagine($data['img'] ?? null),
|
||||
]);
|
||||
|
||||
|
||||
@@ -5,18 +5,21 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Modello;
|
||||
use App\Models\Sistema;
|
||||
use App\Support\Configuratore\CalcolatorePrezzo;
|
||||
use App\Support\Configuratore\ConfigurazioneModello;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CatalogoController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /api/sistemi
|
||||
* Sistemi attivi con i rispettivi modelli attivi, nell'ordine del menu.
|
||||
* Sistemi attivi con i rispettivi modelli attivi e con listino, nell'ordine del menu.
|
||||
*/
|
||||
public function sistemi(): JsonResponse
|
||||
{
|
||||
$sistemi = Sistema::attivi()
|
||||
->with(['modelli' => fn ($q) => $q->attivi()])
|
||||
->with(['modelli' => fn ($q) => $q->nelConfiguratore()])
|
||||
->get();
|
||||
|
||||
return response()->json($sistemi->map(fn (Sistema $s) => $this->sistema($s))->values());
|
||||
@@ -29,7 +32,7 @@ class CatalogoController extends Controller
|
||||
{
|
||||
$sistema = Sistema::attivi()
|
||||
->where('codice', $codice)
|
||||
->with(['modelli' => fn ($q) => $q->attivi()])
|
||||
->with(['modelli' => fn ($q) => $q->nelConfiguratore()])
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json($this->sistema($sistema));
|
||||
@@ -41,12 +44,7 @@ class CatalogoController extends Controller
|
||||
*/
|
||||
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()
|
||||
$prezzi = $this->trovaModello($codice, $modello)->listino()
|
||||
->orderBy('altezza')
|
||||
->orderBy('larghezza')
|
||||
->get(['altezza', 'larghezza', 'prezzo'])
|
||||
@@ -55,6 +53,57 @@ class CatalogoController extends Controller
|
||||
return response()->json($prezzi);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sistemi/{codice}/modelli/{modello}/configurazione
|
||||
* Menu, finiture, vetri, opzioni e misure del modello per il configuratore.
|
||||
*/
|
||||
public function configurazione(string $codice, string $modello, ConfigurazioneModello $configurazione): JsonResponse
|
||||
{
|
||||
return response()->json($configurazione->perModello($this->trovaModello($codice, $modello)));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sistemi/{codice}/modelli/{modello}/prezzo
|
||||
* Prezzo (IVA esclusa) della configurazione scelta; 422 se la misura è fuori listino.
|
||||
*/
|
||||
public function prezzo(Request $request, string $codice, string $modello, CalcolatorePrezzo $calcolatore): JsonResponse
|
||||
{
|
||||
$scelte = $request->validate(self::regoleScelte());
|
||||
|
||||
return response()->json($calcolatore->calcola($this->trovaModello($codice, $modello), $scelte));
|
||||
}
|
||||
|
||||
/** Validazione delle scelte del configuratore, condivisa con il carrello. */
|
||||
public static function regoleScelte(string $prefisso = ''): array
|
||||
{
|
||||
return [
|
||||
"{$prefisso}larghezza" => ['required', 'integer', 'min:1', 'max:100000'],
|
||||
"{$prefisso}altezza" => ['required', 'integer', 'min:1', 'max:100000'],
|
||||
"{$prefisso}finitura_interna" => ['nullable', 'string', 'max:50'],
|
||||
"{$prefisso}finitura_esterna" => ['nullable', 'string', 'max:50'],
|
||||
"{$prefisso}maniglia" => ['nullable', 'string', 'max:50'],
|
||||
"{$prefisso}vetro" => ['nullable', 'string', 'max:50'],
|
||||
"{$prefisso}opzioni" => ['nullable', 'array'],
|
||||
// Un codice per i gruppi a scelta singola, una lista di codici per quelli multipli.
|
||||
"{$prefisso}opzioni.*" => ['nullable', function (string $campo, mixed $valore, \Closure $errore) {
|
||||
$codici = is_array($valore) ? $valore : [$valore];
|
||||
if (count($codici) > 50 || collect($codici)->contains(fn ($c) => ! is_string($c) || mb_strlen($c) > 100)) {
|
||||
$errore('Scelta non valida.');
|
||||
}
|
||||
}],
|
||||
];
|
||||
}
|
||||
|
||||
/** Modello attivo e con listino di un sistema attivo, cercato per codici (404 altrimenti). */
|
||||
public static function trovaModello(string $codice, string $modello): Modello
|
||||
{
|
||||
return Modello::nelConfiguratore()
|
||||
->where('codice', $modello)
|
||||
->whereHas('sistema', fn ($q) => $q->where('codice', $codice)->where('attivo', true))
|
||||
->with('sistema', 'regolaVetro')
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
private function sistema(Sistema $sistema): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\Configuratore\Media;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /api/media/{percorso}
|
||||
* Immagini del configuratore dal disco public, con gli header CORS dell'API (necessari
|
||||
* alle texture WebGL caricate da un'altra origine).
|
||||
*/
|
||||
public function __invoke(string $percorso): BinaryFileResponse
|
||||
{
|
||||
$disco = Storage::disk('public');
|
||||
|
||||
abort_if(
|
||||
str_contains($percorso, '..')
|
||||
|| ! Str::startsWith($percorso, Media::CARTELLE)
|
||||
|| ! $disco->exists($percorso),
|
||||
404
|
||||
);
|
||||
|
||||
return response()->file($disco->path($percorso), [
|
||||
'Cache-Control' => 'public, max-age=86400',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user