From 31d5152d8c4cb28b36af133332465e25fc82cbbf Mon Sep 17 00:00:00 2001 From: Carmine Savino Date: Mon, 19 Oct 2026 15:48:15 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Aggiungi=20funzionalit=C3=A0=20di=20eli?= =?UTF-8?q?minazione=20per=20clienti=20e=20preventivi=20con=20gestione=20d?= =?UTF-8?q?elle=20dipendenze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Filament/Resources/ClienteResource.php | 7 +- .../ClienteResource/Pages/EditCliente.php | 3 +- .../ClienteResource/Pages/ViewCliente.php | 2 + .../Filament/Resources/PreventivoResource.php | 21 +- .../Controllers/Api/CarrelloController.php | 18 +- backoffice/app/Models/Cliente.php | 17 ++ backoffice/app/Models/Preventivo.php | 19 ++ backoffice/cliente/dist/index.html | 2 +- backoffice/cliente/src/App.vue | 2 +- backoffice/cliente/src/api.js | 13 +- ...hange_preventivi_cliente_fk_to_cascade.php | 34 +++ .../views/emails/preventivo-cliente.blade.php | 2 +- .../resources/views/pdf/preventivo.blade.php | 2 +- .../tests/Feature/ConfermaPreventivoTest.php | 141 +++++++++++++ .../tests/Feature/EliminazioneClienteTest.php | 96 +++++++++ dev.sh | 89 ++++++++ docs/AVVIO_LOCALE.md | 195 ++++++++++++++++++ src/js/configurator.js | 2 +- src/js/price_calculator.js | 3 +- src/js/starter.js | 22 +- 20 files changed, 673 insertions(+), 17 deletions(-) create mode 100644 backoffice/database/migrations/2026_08_24_183000_change_preventivi_cliente_fk_to_cascade.php create mode 100644 backoffice/tests/Feature/ConfermaPreventivoTest.php create mode 100644 backoffice/tests/Feature/EliminazioneClienteTest.php create mode 100755 dev.sh create mode 100644 docs/AVVIO_LOCALE.md diff --git a/backoffice/app/Filament/Resources/ClienteResource.php b/backoffice/app/Filament/Resources/ClienteResource.php index 8d1db14..954f14a 100644 --- a/backoffice/app/Filament/Resources/ClienteResource.php +++ b/backoffice/app/Filament/Resources/ClienteResource.php @@ -97,10 +97,15 @@ class ClienteResource extends Resource ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make() + ->modalDescription('Verrà eliminato definitivamente il cliente insieme a tutti i suoi preventivi e ai file associati (PDF, immagini)') + ->successRedirectUrl(static::getUrl('index')), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->modalDescription('I clienti selezionati verranno eliminati definitivamente insieme a tutti i loro preventivi e ai file associati (PDF, immagini)') + ->successRedirectUrl(static::getUrl('index')), ]), ]); } diff --git a/backoffice/app/Filament/Resources/ClienteResource/Pages/EditCliente.php b/backoffice/app/Filament/Resources/ClienteResource/Pages/EditCliente.php index 69298da..a3bf81c 100644 --- a/backoffice/app/Filament/Resources/ClienteResource/Pages/EditCliente.php +++ b/backoffice/app/Filament/Resources/ClienteResource/Pages/EditCliente.php @@ -17,7 +17,8 @@ class EditCliente extends EditRecord { return [ Actions\ViewAction::make(), - Actions\DeleteAction::make(), + Actions\DeleteAction::make() + ->modalDescription('Verrà eliminato definitivamente il cliente insieme a tutti i suoi preventivi e ai file associati (PDF, immagini).'), ]; } diff --git a/backoffice/app/Filament/Resources/ClienteResource/Pages/ViewCliente.php b/backoffice/app/Filament/Resources/ClienteResource/Pages/ViewCliente.php index bf5b73c..4d4c987 100644 --- a/backoffice/app/Filament/Resources/ClienteResource/Pages/ViewCliente.php +++ b/backoffice/app/Filament/Resources/ClienteResource/Pages/ViewCliente.php @@ -14,6 +14,8 @@ class ViewCliente extends ViewRecord { return [ Actions\EditAction::make(), + Actions\DeleteAction::make() + ->modalDescription('Verrà eliminato definitivamente il cliente insieme a tutti i suoi preventivi e ai file associati (PDF, immagini).'), ]; } } diff --git a/backoffice/app/Filament/Resources/PreventivoResource.php b/backoffice/app/Filament/Resources/PreventivoResource.php index 19ae337..51c4745 100644 --- a/backoffice/app/Filament/Resources/PreventivoResource.php +++ b/backoffice/app/Filament/Resources/PreventivoResource.php @@ -4,6 +4,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\PreventivoResource\Pages; use App\Filament\Resources\PreventivoResource\RelationManagers\ItemsRelationManager; +use App\Models\Cliente; use App\Models\Preventivo; use App\Support\PreventivoPdf; use Filament\Forms; @@ -44,7 +45,13 @@ class PreventivoResource extends Resource Forms\Components\Select::make('cliente_id') ->label('Cliente') ->relationship('cliente', 'nome') - ->searchable() + ->getOptionLabelFromRecordUsing(function (Cliente $record): string { + $nome = trim("{$record->nome} {$record->cognome}"); + $parti = array_filter([$record->ragione_sociale, $nome]); + + return $parti ? implode(' — ', $parti) : (string) $record->username; + }) + ->searchable(['ragione_sociale', 'nome', 'cognome']) ->preload(), ]); } @@ -58,9 +65,14 @@ class PreventivoResource extends Resource ->label('N.') ->sortable() ->placeholder('—'), - Tables\Columns\TextColumn::make('cliente.nominativo') + Tables\Columns\TextColumn::make('cliente.ragione_sociale') + ->label('Ragione sociale') + ->searchable() + ->toggleable() + ->placeholder('—'), + Tables\Columns\TextColumn::make('cliente.nome_completo') ->label('Cliente') - ->searchable(['nome', 'cognome', 'ragione_sociale']) + ->searchable(['nome', 'cognome']) ->placeholder('—'), Tables\Columns\TextColumn::make('cliente.email') ->label('Email') @@ -116,7 +128,8 @@ class PreventivoResource extends Resource ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->successRedirectUrl(static::getUrl('index')), ]), ]); } diff --git a/backoffice/app/Http/Controllers/Api/CarrelloController.php b/backoffice/app/Http/Controllers/Api/CarrelloController.php index 31a953c..49395db 100644 --- a/backoffice/app/Http/Controllers/Api/CarrelloController.php +++ b/backoffice/app/Http/Controllers/Api/CarrelloController.php @@ -12,6 +12,7 @@ use App\Support\ConfigurazioneFormatter; use App\Support\PreventivoPdf; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; @@ -122,8 +123,21 @@ class CarrelloController extends Controller 'note' => ['nullable', 'string', 'max:2000'], ]); - DB::transaction(function () use ($preventivo, $dati) { - $cliente = Cliente::create($dati); + // 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(); + + DB::transaction(function () use ($preventivo, $dati, $clienteLoggato) { + $cliente = $clienteLoggato; + + if ($cliente) { + $cliente->update($dati); + } else { + $cliente = Cliente::create($dati); + } $preventivo->cliente_id = $cliente->id; $preventivo->stato = 'inviato'; diff --git a/backoffice/app/Models/Cliente.php b/backoffice/app/Models/Cliente.php index deb452d..bd73e0d 100644 --- a/backoffice/app/Models/Cliente.php +++ b/backoffice/app/Models/Cliente.php @@ -31,6 +31,16 @@ class Cliente extends Model return $this->hasMany(Preventivo::class); } + protected static function booted(): void + { + static::deleting(function (Cliente $cliente): void { + // Elimina di conseguenza i preventivi del cliente (con i relativi file) + // e revoca i token API usati per l'accesso al configuratore. + $cliente->preventivi()->get()->each->delete(); + $cliente->tokens()->delete(); + }); + } + public function getNominativoAttribute(): string { if ($this->ragione_sociale) { @@ -41,4 +51,11 @@ class Cliente extends Model return $nome !== '' ? $nome : $this->username; } + + public function getNomeCompletoAttribute(): string + { + $nome = trim("{$this->nome} {$this->cognome}"); + + return $nome !== '' ? $nome : $this->username; + } } diff --git a/backoffice/app/Models/Preventivo.php b/backoffice/app/Models/Preventivo.php index 026249c..d2deb2d 100644 --- a/backoffice/app/Models/Preventivo.php +++ b/backoffice/app/Models/Preventivo.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Storage; class Preventivo extends Model { @@ -26,6 +27,24 @@ class Preventivo extends Model 'chiuso' => 'Chiuso', ]; + protected static function booted(): void + { + static::deleting(function (Preventivo $preventivo): void { + // Elimina immagini degli item (disk 'public') e PDF (disk 'local') prima della cancellazione. + foreach ($preventivo->items()->get() as $item) { + if ($item->immagine_path && Storage::disk('public')->exists($item->immagine_path)) { + Storage::disk('public')->delete($item->immagine_path); + } + + $item->delete(); + } + + if ($preventivo->pdf_path && Storage::disk('local')->exists($preventivo->pdf_path)) { + Storage::disk('local')->delete($preventivo->pdf_path); + } + }); + } + public function cliente(): BelongsTo { return $this->belongsTo(Cliente::class); diff --git a/backoffice/cliente/dist/index.html b/backoffice/cliente/dist/index.html index 95e030f..bb7cc59 100644 --- a/backoffice/cliente/dist/index.html +++ b/backoffice/cliente/dist/index.html @@ -5,7 +5,7 @@ Il tuo preventivo · Krèios - + diff --git a/backoffice/cliente/src/App.vue b/backoffice/cliente/src/App.vue index 74cdb74..ca938b8 100644 --- a/backoffice/cliente/src/App.vue +++ b/backoffice/cliente/src/App.vue @@ -146,7 +146,7 @@ onMounted(loadCarrello)
- Totale IVA 22% inclusa + Totale Prezzi IVA esclusa {{ formatEuro(carrello.totale) }}
diff --git a/backoffice/cliente/src/api.js b/backoffice/cliente/src/api.js index 4ec47f3..01894d1 100644 --- a/backoffice/cliente/src/api.js +++ b/backoffice/cliente/src/api.js @@ -22,8 +22,17 @@ async function request(path, options = {}) { export const api = { getCarrello: (token) => request(`/carrello/${token}`), rimuoviItem: (token, itemId) => request(`/carrello/${token}/item/${itemId}`, { method: 'DELETE' }), - conferma: (token, dati) => - request(`/carrello/${token}/conferma`, { method: 'POST', body: JSON.stringify(dati) }), + // Se l'utente era loggato nel configuratore, passiamo il suo token al backend così + // il preventivo viene agganciato al suo record cliente esistente invece di crearne + // uno nuovo (è lo stesso token usato per precompilare l'anagrafica). + conferma: (token, dati) => { + const headers = {} + const clienteToken = resolveClienteToken() + if (clienteToken) { + headers.Authorization = `Bearer ${clienteToken}` + } + return request(`/carrello/${token}/conferma`, { method: 'POST', body: JSON.stringify(dati), headers }) + }, // Dati anagrafici del cliente loggato nel configuratore, per precompilare il checkout. me: (clienteToken) => request('/auth/me', { headers: { Authorization: `Bearer ${clienteToken}` } }), diff --git a/backoffice/database/migrations/2026_08_24_183000_change_preventivi_cliente_fk_to_cascade.php b/backoffice/database/migrations/2026_08_24_183000_change_preventivi_cliente_fk_to_cascade.php new file mode 100644 index 0000000..d05b4f2 --- /dev/null +++ b/backoffice/database/migrations/2026_08_24_183000_change_preventivi_cliente_fk_to_cascade.php @@ -0,0 +1,34 @@ +dropForeign(['cliente_id']); + }); + + Schema::table('preventivi', function (Blueprint $table) { + $table->foreign('cliente_id')->references('id')->on('clienti')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::table('preventivi', function (Blueprint $table) { + $table->dropForeign(['cliente_id']); + }); + + Schema::table('preventivi', function (Blueprint $table) { + $table->foreign('cliente_id')->references('id')->on('clienti')->nullOnDelete(); + }); + } +}; \ No newline at end of file diff --git a/backoffice/resources/views/emails/preventivo-cliente.blade.php b/backoffice/resources/views/emails/preventivo-cliente.blade.php index 89667fc..7ebe80f 100644 --- a/backoffice/resources/views/emails/preventivo-cliente.blade.php +++ b/backoffice/resources/views/emails/preventivo-cliente.blade.php @@ -12,7 +12,7 @@ Ciao {{ $preventivo->cliente?->nome }}, abbiamo ricevuto la tua richiesta di preventivo **n. {{ $preventivo->numero }}**. In allegato trovi il **PDF riassuntivo** con le configurazioni che hai selezionato e il totale -indicativo (IVA 22% inclusa). +indicativo (prezzi IVA esclusa). Un nostro consulente ti ricontatterà al più presto per finalizzare l'offerta. diff --git a/backoffice/resources/views/pdf/preventivo.blade.php b/backoffice/resources/views/pdf/preventivo.blade.php index 4469c2f..098074a 100644 --- a/backoffice/resources/views/pdf/preventivo.blade.php +++ b/backoffice/resources/views/pdf/preventivo.blade.php @@ -100,6 +100,6 @@ @endforeach
Totale: € {{ number_format($preventivo->totale, 2, ',', '.') }}
-
IVA 22% inclusa. Preventivo indicativo, salvo verifica tecnica e rilievo misure.
+
Prezzi IVA esclusa. Preventivo indicativo, salvo verifica tecnica e rilievo misure.
diff --git a/backoffice/tests/Feature/ConfermaPreventivoTest.php b/backoffice/tests/Feature/ConfermaPreventivoTest.php new file mode 100644 index 0000000..8f17eb0 --- /dev/null +++ b/backoffice/tests/Feature/ConfermaPreventivoTest.php @@ -0,0 +1,141 @@ + 'token-test-123', + 'stato' => 'bozza', + 'totale' => 0, + ]); + + $preventivo->items()->create([ + 'sistema' => 'pvc', + 'type' => 'finestra_1_anta', + 'nome_modello' => 'Finestra 1 anta', + 'larghezza' => 100, + 'altezza' => 100, + 'quantita' => 1, + 'prezzo_unitario' => 500, + 'prezzo_totale' => 500, + 'payload' => ['nome' => 'Finestra 1 anta'], + ]); + + $preventivo->ricalcolaTotale(); + + return $preventivo; + } + + private function datiConferma(): array + { + return [ + 'nome' => 'Mario', + 'cognome' => 'Rossi', + 'ragione_sociale' => null, + 'email' => 'mario.rossi@example.com', + 'telefono' => '3331234567', + 'indirizzo' => 'Via Roma 1', + 'cap' => '20100', + 'citta' => 'Milano', + 'provincia' => 'MI', + 'note' => null, + ]; + } + + public function test_conferma_da_cliente_loggato_non_crea_un_nuovo_cliente(): void + { + Mockery::mock('alias:App\Support\PreventivoPdf') + ->shouldReceive('genera') + ->andReturn('preventivi/preventivo-1.pdf'); + Mail::fake(); + + $cliente = Cliente::create([ + 'username' => 'user1234', + 'password' => 'Segreta123!', + 'nome' => 'Mario', + 'cognome' => 'Bianchi', + 'email' => 'mario.bianchi@example.com', + 'telefono' => '3330000000', + ]); + $token = $cliente->createToken('configuratore', ['*'])->plainTextToken; + + $preventivo = $this->creaCarrelloBozza(); + + $dati = $this->datiConferma(); + $dati['cognome'] = 'Rossi'; // diverso dal dato salvato: l'anagrafica deve aggiornarsi + + $response = $this->postJson("/api/carrello/{$preventivo->token}/conferma", $dati, [ + 'Authorization' => "Bearer {$token}", + ]); + + $response->assertOk(); + + // Nessun nuovo utente creato: in database c'è ancora solo il cliente originale. + $this->assertSame(1, Cliente::count()); + + $preventivo->refresh(); + $this->assertSame($cliente->id, $preventivo->cliente_id); + $this->assertSame('inviato', $preventivo->stato); + $this->assertNotNull($preventivo->numero); + + // L'anagrafica del cliente loggato è aggiornata con i dati confermati nel form. + $this->assertSame('Rossi', $cliente->fresh()->cognome); + $this->assertSame('3331234567', $cliente->fresh()->telefono); + + Mail::assertSent(PreventivoCliente::class); + } + + public function test_conferma_da_guest_crea_il_cliente(): void + { + Mockery::mock('alias:App\Support\PreventivoPdf') + ->shouldReceive('genera') + ->andReturn('preventivi/preventivo-1.pdf'); + Mail::fake(); + + $this->assertSame(0, Cliente::count()); + $preventivo = $this->creaCarrelloBozza(); + + $response = $this->postJson("/api/carrello/{$preventivo->token}/conferma", $this->datiConferma()); + + $response->assertOk(); + + $this->assertSame(1, Cliente::count()); + + $preventivo->refresh(); + $this->assertNotNull($preventivo->cliente_id); + $this->assertSame('mario.rossi@example.com', $preventivo->cliente->email); + } + + public function test_conferma_con_token_non_valido_tratta_l_utente_come_guest(): void + { + Mockery::mock('alias:App\Support\PreventivoPdf') + ->shouldReceive('genera') + ->andReturn('preventivi/preventivo-1.pdf'); + Mail::fake(); + + $preventivo = $this->creaCarrelloBozza(); + + $response = $this->postJson("/api/carrello/{$preventivo->token}/conferma", $this->datiConferma(), [ + 'Authorization' => 'Bearer token-non-esistente', + ]); + + $response->assertOk(); + + $this->assertSame(1, Cliente::count()); + $this->assertNotNull($preventivo->fresh()->cliente_id); + } +} diff --git a/backoffice/tests/Feature/EliminazioneClienteTest.php b/backoffice/tests/Feature/EliminazioneClienteTest.php new file mode 100644 index 0000000..f22825a --- /dev/null +++ b/backoffice/tests/Feature/EliminazioneClienteTest.php @@ -0,0 +1,96 @@ +actingAs(User::factory()->create()); + } + + private function creaCliente(string $nome, string $username): Cliente + { + return Cliente::create([ + 'username' => $username, + 'password' => 'Segreta123!', + 'nome' => $nome, + 'email' => strtolower($username).'@example.com', + ]); + } + + private function creaPreventivoInviato(Cliente $cliente): Preventivo + { + return Preventivo::create([ + 'cliente_id' => $cliente->id, + 'token' => Str::random(40), + 'stato' => 'inviato', + 'totale' => 100, + ]); + } + + public function test_eliminazione_singola_ridireziona_alla_lista(): void + { + $cliente = $this->creaCliente('Mario', 'mario-test'); + $preventivo = $this->creaPreventivoInviato($cliente); + + Livewire::test(ListClientes::class) + ->callTableAction('delete', $cliente) + ->assertRedirect(ClienteResource::getUrl('index')); + + $this->assertDatabaseMissing(Cliente::class, ['id' => $cliente->id]); + $this->assertDatabaseMissing(Preventivo::class, ['id' => $preventivo->id]); + } + + public function test_eliminazione_massiva_ridireziona_alla_lista(): void + { + $primo = $this->creaCliente('Mario', 'mario-bulk'); + $secondo = $this->creaCliente('Luigi', 'luigi-bulk'); + $this->creaPreventivoInviato($primo); + $this->creaPreventivoInviato($secondo); + + Livewire::test(ListClientes::class) + ->callTableBulkAction('delete', [$primo, $secondo]) + ->assertRedirect(ClienteResource::getUrl('index')); + + $this->assertSame(0, Cliente::count()); + $this->assertSame(0, Preventivo::count()); + } + + public function test_badge_preventivi_aggiornato_dopo_eliminazione(): void + { + $conPreventivo = $this->creaCliente('Anna', 'anna-test'); + $senzaPreventivi = $this->creaCliente('Piero', 'pier-test'); + $this->creaPreventivoInviato($conPreventivo); + $this->creaPreventivoInviato($senzaPreventivi); + + $this->assertSame(2, (int) PreventivoResource::getNavigationBadge()); + + Livewire::test(ListClientes::class) + ->callTableAction('delete', $conPreventivo) + ->assertRedirect(ClienteResource::getUrl('index')); + + // Dopo l'eliminazione il conteggio ricalcolato dal database è aggiornato. + $this->assertSame(1, (int) PreventivoResource::getNavigationBadge()); + } +} \ No newline at end of file diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..4cabc26 --- /dev/null +++ b/dev.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Avvia l'ambiente di sviluppo completo (vedi docs/AVVIO_LOCALE.md): +# - backoffice Laravel (API + admin) -> http://localhost:8000 +# - app cliente preventivi (Vue) -> http://localhost:5199 +# - configuratore 3D (Vite) -> http://localhost:5173 +# Ctrl+C ferma tutti i servizi. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +BACKOFFICE="$ROOT/backoffice" +CLIENTE="$BACKOFFICE/cliente" + +port_in_use() { lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1; } + +# --- Controlli preliminari --------------------------------------------------- +for cmd in php pnpm lsof; do + command -v "$cmd" >/dev/null || { echo "Errore: '$cmd' non trovato nel PATH." >&2; exit 1; } +done + +if ! port_in_use 3306; then + echo "Errore: MySQL non risponde sulla porta 3306. Avvialo (es. da Herd) e riprova." >&2 + exit 1 +fi + +for port in 8000 5199 5173; do + if port_in_use "$port"; then + echo "Errore: la porta $port è già occupata:" >&2 + lsof -nP -iTCP:"$port" -sTCP:LISTEN >&2 + exit 1 + fi +done + +[ -d "$BACKOFFICE/vendor" ] || (cd "$BACKOFFICE" && composer install) +[ -d "$CLIENTE/node_modules" ] || (cd "$CLIENTE" && pnpm install) +[ -d "$ROOT/node_modules" ] || (cd "$ROOT" && pnpm install) + +# --- Avvio servizi ----------------------------------------------------------- +PIDS=() + +# Termina un processo e tutti i suoi discendenti (pnpm -> vite, artisan serve -> php -S). +kill_tree() { + local child + for child in $(pgrep -P "$1" 2>/dev/null); do kill_tree "$child"; done + kill -TERM "$1" 2>/dev/null || true +} + +cleanup() { + trap - INT TERM EXIT + echo + echo "Arresto dei servizi..." + for pid in "${PIDS[@]}"; do kill_tree "$pid"; done + wait 2>/dev/null || true +} +trap cleanup EXIT +trap 'exit 0' INT TERM + +# Esegue un comando in una cartella prefissando ogni riga di output con un'etichetta. +run() { + local label="$1" dir="$2"; shift 2 + (cd "$dir" && exec "$@") > >(sed -u "s/^/[$label] /") 2>&1 & + PIDS+=($!) +} + +run "laravel" "$BACKOFFICE" php artisan serve --port=8000 +run "cliente" "$CLIENTE" pnpm dev +run "config " "$ROOT" pnpm dev + +cat </dev/null; then + echo "Un servizio si è fermato, arresto degli altri." >&2 + exit 1 + fi + done + sleep 1 +done diff --git a/docs/AVVIO_LOCALE.md b/docs/AVVIO_LOCALE.md new file mode 100644 index 0000000..b27625d --- /dev/null +++ b/docs/AVVIO_LOCALE.md @@ -0,0 +1,195 @@ +# Avvio locale — Krèios + +Guida per avviare l'ambiente completo di sviluppo: **configuratore 3D**, **backoffice Laravel** (API + pannello admin) e **app cliente** per i preventivi. + +## Panoramica + +| Componente | Cartella | Stack | Porta | URL dev | +|---|---|---|---|---| +| Configuratore 3D | `.` (root del repo) | Vite 6 + Three.js + jQuery | **5173** | | +| Backoffice (API + admin) | `backoffice/` | Laravel 13 + Filament 3 + MySQL | **8000** | , admin su | +| App cliente preventivi | `backoffice/cliente/` | Vue 3 + Vite | **5199** | | + +Le tre porte sono **fisse** (`strictPort: true` nei vari `vite.config.js` / `serve --port`): se una porta è già occupata l'avvio fallisce con un errore esplicito invece di spostarsi silenziosamente su un'altra. + +**Come si collegano:** + +- Il configuratore chiama l'API Laravel per il carrello (`POST /api/carrello/aggiungi`, ecc.). +- L'URL dell'app cliente **non è hardcoded** nel configuratore: all'avvio chiama `GET /api/config` e usa il `frontend_cliente_url` restituito da Laravel (letto da `APP_FRONTEND_CLIENTE_URL` nel `.env` del backoffice). +- Il flusso completo è descritto in [PREVENTIVI_BACKOFFICE.md](PREVENTIVI_BACKOFFICE.md). + +## Requisiti + +| Strumento | Versione minima | Note | +|---|---|---| +| Node.js | 20+ | per i tre front (Vite) | +| pnpm | 8+ | package manager della root e dell'app cliente (`pnpm-lock.yaml` è il lockfile di riferimento) — `npm i -g pnpm` o `corepack enable` | +| PHP | 8.3+ | richiesta da `composer.json` (`"php": "^8.3"`) | +| Composer | 2+ | dipendenze Laravel | +| MySQL | 8 | host `127.0.0.1`, user `root`, **nessuna password** | +| Python 3 | 3.10+ | **opzionale**: solo per rigenerare i listini PVC | + +## 1) Backoffice Laravel (API + admin) — porta 8000 + +```bash +cd backoffice +composer install +``` + +### Database + +Creare il database (una volta sola): + +```sql +CREATE DATABASE kreios_preventivi CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +### `.env` + +Il backoffice ignora il suo `.env` in git: in un clone fresco va creato: + +```bash +cp .env.example .env +php artisan key:generate +``` + +Poi impostare le variabili di sviluppo (in questo repo il `.env` è già configurato così): + +```ini +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=kreios_preventivi +DB_USERNAME=root +DB_PASSWORD= + +APP_URL=http://localhost:8000 +APP_FRONTEND_CLIENTE_URL=http://localhost:5199 +ADMIN_NOTIFY_EMAIL=tua-email@example.com +MAIL_MAILER=log +``` + +| Variabile | Descrizione | +|---|---| +| `APP_URL` | Base URL del backoffice | +| `APP_FRONTEND_CLIENTE_URL` | URL dell'app cliente — restituito al configuratore da `GET /api/config` | +| `ADMIN_NOTIFY_EMAIL` | Email che riceve la notifica di nuovo preventivo | +| `CONFIGURATOR_SESSION_DAYS` | Durata (giorni) della sessione di login del configuratore | +| `MAIL_MAILER` | `log` in dev: le email finiscono in `storage/logs/laravel.log` (incluso il PDF allegato) | + +### Migrations, seed e asset + +```bash +php artisan migrate --seed # crea le tabelle + l'utente admin +php artisan storage:link +npm install # asset Vite del pannello (nessun lockfile; .npmrc ha ignore-scripts=true) +``` + +### Avvio + +```bash +php artisan serve --port=8000 +``` + +- API: +- Pannello admin: + - Email: `admin@kreiosinfissi.it` — Password: `password` (seed di default) +- Le email di conferma preventivo sono inviate **in modo sincrono** (`Mail::send`): in dev non serve avviare `queue:listen`. + +> Alternativa: `composer dev` avvia in parallelo server, queue, log (`pail`) e Vite. + +## 2) App cliente (Vue) — porta 5199 + +```bash +cd backoffice/cliente +pnpm install +pnpm dev +``` + +`.env` già presente in repo con il default corretto per lo sviluppo: `VITE_API_BASE=http://localhost:8000/api`. + +## 3) Configuratore 3D — porta 5173 + +```bash +pnpm install +pnpm dev +``` + +- Il server parte su `0.0.0.0:5173` (vedi `vite.config.js`). +- L'entry point **richiede due parametri obbligatori** nella query string: + + ``` + http://localhost:5173/?sistema=pvc&type=finestra_2_ante + ``` + + - `sistema`: `pvc` | `alluminio` | `legno` | `acciaio` + - `type`: id modello valido per quel sistema (elenco completo in [URL_PARAMS.md](URL_PARAMS.md)) + + Senza i due parametri l'app lancia un errore in fase di avvio. +- In produzione il configuratore gira dentro un `