validate([ 'token' => ['nullable', 'string', 'max:64'], 'sistema' => ['required', 'string', 'max:50'], 'type' => ['required', 'string', 'max:100'], 'qty' => ['nullable', 'integer', 'min:1', 'max:999'], '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 = $calcolo['prezzo']; $configurazione = $calcolo['configurazione']; $item = new PreventivoItem([ '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' => $descrittive + $configurazione + [ 'scelte' => $calcolo['scelte'], 'dettaglio_prezzo' => $calcolo['dettaglio'], ], 'immagine_path' => $this->salvaImmagine($data['img'] ?? null), ]); $preventivo->items()->save($item); $preventivo->ricalcolaTotale(); return response()->json([ 'token' => $preventivo->token, 'carrello' => $this->serializzaCarrello($preventivo->fresh('items')), ], 201); } /** GET /api/carrello/{token} */ public function mostra(string $token): JsonResponse { $preventivo = Preventivo::where('token', $token)->where('stato', 'bozza') ->with('items')->first(); if (! $preventivo) { return response()->json(['token' => $token, 'carrello' => ['items' => [], 'totale' => 0]]); } return response()->json([ 'token' => $preventivo->token, 'carrello' => $this->serializzaCarrello($preventivo), ]); } /** DELETE /api/carrello/{token}/item/{item} */ public function rimuoviItem(string $token, int $item): JsonResponse { $preventivo = Preventivo::where('token', $token)->where('stato', 'bozza') ->with('items')->firstOrFail(); $preventivo->items()->whereKey($item)->delete(); $preventivo->ricalcolaTotale(); return response()->json([ 'token' => $preventivo->token, 'carrello' => $this->serializzaCarrello($preventivo->fresh('items')), ]); } /** * POST /api/carrello/{token}/conferma * Raccoglie i dati cliente, finalizza il preventivo, genera il PDF e invia le email. */ public function conferma(Request $request, string $token): JsonResponse { $preventivo = Preventivo::where('token', $token)->where('stato', 'bozza') ->with('items')->firstOrFail(); if ($preventivo->items->isEmpty()) { return response()->json(['message' => 'Il carrello è vuoto.'], 422); } $dati = $request->validate([ 'nome' => ['required', 'string', 'max:255'], 'cognome' => ['nullable', 'string', 'max:255'], 'ragione_sociale' => ['nullable', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255'], 'telefono' => ['nullable', 'string', 'max:50'], 'indirizzo' => ['nullable', 'string', 'max:255'], 'cap' => ['nullable', 'string', 'max:10'], 'citta' => ['nullable', 'string', 'max:255'], 'provincia' => ['nullable', 'string', 'max:5'], 'note' => ['nullable', 'string', 'max:2000'], ]); // Oltre il massimo di pezzi il trasporto non si sceglie: è da quantificare. $trasporto = Preventivo::oltreMaxPezzi((int) $preventivo->items->sum('quantita')) ? Preventivo::TRASPORTO_OLTRE_MAX : $request->validate([ 'trasporto' => ['required', 'string', Rule::in(array_keys(config('preventivi.trasporto.zone')))], ], [ 'trasporto.required' => 'Seleziona il trasporto.', ])['trasporto']; // Se la richiesta arriva da un cliente loggato nel configuratore (token Sanctum // valido), il preventivo va agganciato al suo record esistente — aggiornando // l'anagrafica con i dati confermati nel form — e NON a un nuovo cliente. // Il guard Sanctum legge il token Bearer direttamente dalla richiesta: // per i guest (o token assenti/scaduti/non validi) restituisce null. $clienteLoggato = Auth::guard('sanctum')->user(); // Sconti solo per i clienti loggati, entro i massimi decisi dall'amministratore. $massimi = $clienteLoggato?->scontiMassimi() ?? ['max_1' => 0, 'max_2' => 0]; $sconti = $request->validate([ 'sconto_1' => ['nullable', 'numeric', 'min:0', 'max:'.$massimi['max_1']], 'sconto_2' => ['nullable', 'numeric', 'min:0', 'max:'.$massimi['max_2']], ], [ 'sconto_1.max' => 'Il 1° sconto non può superare il :max%.', 'sconto_2.max' => 'Lo sconto extra non può superare il :max%.', ]); DB::transaction(function () use ($preventivo, $dati, $clienteLoggato, $sconti, $trasporto) { $cliente = $clienteLoggato; if ($cliente) { $cliente->update($dati); } else { $cliente = Cliente::create($dati); } $preventivo->cliente_id = $cliente->id; $preventivo->stato = 'inviato'; $preventivo->numero = Preventivo::prossimoNumero(); $preventivo->confirmed_at = now(); $preventivo->sconto_1 = round((float) ($sconti['sconto_1'] ?? 0), 2); $preventivo->sconto_2 = round((float) ($sconti['sconto_2'] ?? 0), 2); $preventivo->trasporto = $trasporto; $preventivo->ricalcolaTotale(); }); $preventivo->refresh()->load('cliente', 'items'); // Genera il PDF e salva il path. $pdfPath = PreventivoPdf::genera($preventivo); $preventivo->pdf_path = $pdfPath; $preventivo->save(); // Invia le email (cliente con PDF allegato, admin di notifica). $this->inviaEmail($preventivo); return response()->json([ 'message' => 'Preventivo inviato. Verrai ricontattato al più presto.', 'numero' => $preventivo->numero, ]); } /** Trova il carrello bozza per token o ne crea uno nuovo. */ private function risolviCarrello(?string $token): Preventivo { if ($token) { $preventivo = Preventivo::where('token', $token)->where('stato', 'bozza')->first(); if ($preventivo) { return $preventivo; } } return Preventivo::create([ 'token' => (string) Str::uuid(), 'stato' => 'bozza', 'totale' => 0, ]); } /** Decodifica un data URL base64 e salva il PNG; ritorna il path relativo (disk public). */ private function salvaImmagine(?string $dataUrl): ?string { if (! $dataUrl || ! Str::startsWith($dataUrl, 'data:image')) { return null; } [$meta, $contenuto] = array_pad(explode(',', $dataUrl, 2), 2, null); if ($contenuto === null) { return null; } $binario = base64_decode($contenuto, true); if ($binario === false) { return null; } $path = 'preventivi/items/'.Str::uuid()->toString().'.png'; Storage::disk('public')->put($path, $binario); return $path; } /** @return array{items:array,totale:float} */ private function serializzaCarrello(Preventivo $preventivo): array { return [ 'items' => $preventivo->items->map(fn (PreventivoItem $item) => [ 'id' => $item->id, 'sistema' => $item->sistema, 'type' => $item->type, 'nome_modello' => $item->nome_modello, 'larghezza' => $item->larghezza, 'altezza' => $item->altezza, 'quantita' => $item->quantita, 'prezzo_unitario' => (float) $item->prezzo_unitario, 'prezzo_totale' => (float) $item->prezzo_totale, 'immagine_url' => $item->immagine_path ? Storage::disk('public')->url($item->immagine_path) : null, 'configurazione' => ConfigurazioneFormatter::toRows($item->payload), ])->all(), 'totale' => (float) $preventivo->totale, ]; } private function inviaEmail(Preventivo $preventivo): void { if ($preventivo->cliente?->email) { Mail::to($preventivo->cliente->email)->send(new PreventivoCliente($preventivo)); } $adminEmail = config('preventivi.admin_email'); if ($adminEmail) { Mail::to($adminEmail)->send(new PreventivoAdmin($preventivo)); } } }