backoffice first commit

This commit is contained in:
2026-07-17 11:34:15 +02:00
parent 761db06329
commit 1c40ef6601
159 changed files with 25830 additions and 7875 deletions
@@ -0,0 +1,223 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Mail\PreventivoAdmin;
use App\Mail\PreventivoCliente;
use App\Models\Cliente;
use App\Models\Preventivo;
use App\Models\PreventivoItem;
use App\Support\ConfigurazioneFormatter;
use App\Support\PreventivoPdf;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class CarrelloController extends Controller
{
/**
* POST /api/carrello/aggiungi
* Aggiunge un prodotto configurato al carrello (preventivo in stato bozza).
*/
public function aggiungi(Request $request): 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'],
'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));
}
}
}
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}