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'], 'qty' => ['nullable', 'integer', 'min:1', 'max:999'], 'prezzo' => ['required', 'numeric', 'min:0'], 'payload' => ['nullable', 'array'], 'img' => ['nullable', 'string'], ]); $preventivo = $this->risolviCarrello($data['token'] ?? null); $qty = $data['qty'] ?? 1; $prezzoUnitario = round((float) $data['prezzo'], 2); $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), 'quantita' => $qty, 'prezzo_unitario' => $prezzoUnitario, 'prezzo_totale' => round($prezzoUnitario * $qty, 2), 'payload' => $data['payload'] ?? [], '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'], ]); DB::transaction(function () use ($preventivo, $dati) { $cliente = Cliente::create($dati); $preventivo->cliente_id = $cliente->id; $preventivo->stato = 'inviato'; $preventivo->numero = Preventivo::prossimoNumero(); $preventivo->confirmed_at = now(); $preventivo->save(); }); $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)); } } }