feat: Aggiungi funzionalità di eliminazione per clienti e preventivi con gestione delle dipendenze
This commit is contained in:
@@ -97,10 +97,15 @@ class ClienteResource extends Resource
|
|||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\ViewAction::make(),
|
Tables\Actions\ViewAction::make(),
|
||||||
Tables\Actions\EditAction::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([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
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')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ class EditCliente extends EditRecord
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\ViewAction::make(),
|
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).'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class ViewCliente extends ViewRecord
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\EditAction::make(),
|
Actions\EditAction::make(),
|
||||||
|
Actions\DeleteAction::make()
|
||||||
|
->modalDescription('Verrà eliminato definitivamente il cliente insieme a tutti i suoi preventivi e ai file associati (PDF, immagini).'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources;
|
|||||||
|
|
||||||
use App\Filament\Resources\PreventivoResource\Pages;
|
use App\Filament\Resources\PreventivoResource\Pages;
|
||||||
use App\Filament\Resources\PreventivoResource\RelationManagers\ItemsRelationManager;
|
use App\Filament\Resources\PreventivoResource\RelationManagers\ItemsRelationManager;
|
||||||
|
use App\Models\Cliente;
|
||||||
use App\Models\Preventivo;
|
use App\Models\Preventivo;
|
||||||
use App\Support\PreventivoPdf;
|
use App\Support\PreventivoPdf;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
@@ -44,7 +45,13 @@ class PreventivoResource extends Resource
|
|||||||
Forms\Components\Select::make('cliente_id')
|
Forms\Components\Select::make('cliente_id')
|
||||||
->label('Cliente')
|
->label('Cliente')
|
||||||
->relationship('cliente', 'nome')
|
->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(),
|
->preload(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -58,9 +65,14 @@ class PreventivoResource extends Resource
|
|||||||
->label('N.')
|
->label('N.')
|
||||||
->sortable()
|
->sortable()
|
||||||
->placeholder('—'),
|
->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')
|
->label('Cliente')
|
||||||
->searchable(['nome', 'cognome', 'ragione_sociale'])
|
->searchable(['nome', 'cognome'])
|
||||||
->placeholder('—'),
|
->placeholder('—'),
|
||||||
Tables\Columns\TextColumn::make('cliente.email')
|
Tables\Columns\TextColumn::make('cliente.email')
|
||||||
->label('Email')
|
->label('Email')
|
||||||
@@ -116,7 +128,8 @@ class PreventivoResource extends Resource
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make()
|
||||||
|
->successRedirectUrl(static::getUrl('index')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Support\ConfigurazioneFormatter;
|
|||||||
use App\Support\PreventivoPdf;
|
use App\Support\PreventivoPdf;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@@ -122,8 +123,21 @@ class CarrelloController extends Controller
|
|||||||
'note' => ['nullable', 'string', 'max:2000'],
|
'note' => ['nullable', 'string', 'max:2000'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::transaction(function () use ($preventivo, $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);
|
$cliente = Cliente::create($dati);
|
||||||
|
}
|
||||||
|
|
||||||
$preventivo->cliente_id = $cliente->id;
|
$preventivo->cliente_id = $cliente->id;
|
||||||
$preventivo->stato = 'inviato';
|
$preventivo->stato = 'inviato';
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ class Cliente extends Model
|
|||||||
return $this->hasMany(Preventivo::class);
|
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
|
public function getNominativoAttribute(): string
|
||||||
{
|
{
|
||||||
if ($this->ragione_sociale) {
|
if ($this->ragione_sociale) {
|
||||||
@@ -41,4 +51,11 @@ class Cliente extends Model
|
|||||||
|
|
||||||
return $nome !== '' ? $nome : $this->username;
|
return $nome !== '' ? $nome : $this->username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getNomeCompletoAttribute(): string
|
||||||
|
{
|
||||||
|
$nome = trim("{$this->nome} {$this->cognome}");
|
||||||
|
|
||||||
|
return $nome !== '' ? $nome : $this->username;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class Preventivo extends Model
|
class Preventivo extends Model
|
||||||
{
|
{
|
||||||
@@ -26,6 +27,24 @@ class Preventivo extends Model
|
|||||||
'chiuso' => 'Chiuso',
|
'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
|
public function cliente(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Cliente::class);
|
return $this->belongsTo(Cliente::class);
|
||||||
|
|||||||
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/jpeg" href="/kreios-logo.jpg" />
|
<link rel="icon" type="image/jpeg" href="/kreios-logo.jpg" />
|
||||||
<title>Il tuo preventivo · Krèios</title>
|
<title>Il tuo preventivo · Krèios</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D5nts1N1.js"></script>
|
<script type="module" crossorigin src="/assets/index-CnLA_o5j.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-vT7pz0E4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-vT7pz0E4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ onMounted(loadCarrello)
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="totale-row">
|
<div class="totale-row">
|
||||||
<span>Totale <span class="iva-nota">IVA 22% inclusa</span></span>
|
<span>Totale <span class="iva-nota">Prezzi IVA esclusa</span></span>
|
||||||
<span>{{ formatEuro(carrello.totale) }}</span>
|
<span>{{ formatEuro(carrello.totale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,17 @@ async function request(path, options = {}) {
|
|||||||
export const api = {
|
export const api = {
|
||||||
getCarrello: (token) => request(`/carrello/${token}`),
|
getCarrello: (token) => request(`/carrello/${token}`),
|
||||||
rimuoviItem: (token, itemId) => request(`/carrello/${token}/item/${itemId}`, { method: 'DELETE' }),
|
rimuoviItem: (token, itemId) => request(`/carrello/${token}/item/${itemId}`, { method: 'DELETE' }),
|
||||||
conferma: (token, dati) =>
|
// Se l'utente era loggato nel configuratore, passiamo il suo token al backend così
|
||||||
request(`/carrello/${token}/conferma`, { method: 'POST', body: JSON.stringify(dati) }),
|
// 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.
|
// Dati anagrafici del cliente loggato nel configuratore, per precompilare il checkout.
|
||||||
me: (clienteToken) =>
|
me: (clienteToken) =>
|
||||||
request('/auth/me', { headers: { Authorization: `Bearer ${clienteToken}` } }),
|
request('/auth/me', { headers: { Authorization: `Bearer ${clienteToken}` } }),
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* La cancellazione di un cliente deve eliminare di conseguenza i suoi
|
||||||
|
* preventivi (e gli item, già in cascata): la FK passa da SET NULL a CASCADE.
|
||||||
|
*/
|
||||||
|
public function up(): 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')->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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ Ciao {{ $preventivo->cliente?->nome }},
|
|||||||
abbiamo ricevuto la tua richiesta di preventivo **n. {{ $preventivo->numero }}**.
|
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
|
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.
|
Un nostro consulente ti ricontatterà al più presto per finalizzare l'offerta.
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,6 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
<div class="totale">Totale: € {{ number_format($preventivo->totale, 2, ',', '.') }}</div>
|
<div class="totale">Totale: € {{ number_format($preventivo->totale, 2, ',', '.') }}</div>
|
||||||
<div class="nota">IVA 22% inclusa. Preventivo indicativo, salvo verifica tecnica e rilievo misure.</div>
|
<div class="nota">Prezzi IVA esclusa. Preventivo indicativo, salvo verifica tecnica e rilievo misure.</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Mail\PreventivoCliente;
|
||||||
|
use App\Models\Cliente;
|
||||||
|
use App\Models\Preventivo;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ConfermaPreventivoTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** Crea un preventivo in bozza con una riga, pronto per la conferma. */
|
||||||
|
private function creaCarrelloBozza(): Preventivo
|
||||||
|
{
|
||||||
|
$preventivo = Preventivo::create([
|
||||||
|
'token' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ClienteResource;
|
||||||
|
use App\Filament\Resources\ClienteResource\Pages\ListClientes;
|
||||||
|
use App\Filament\Resources\PreventivoResource;
|
||||||
|
use App\Models\Cliente;
|
||||||
|
use App\Models\Preventivo;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L'eliminazione di un cliente (singola o massiva) deve ridirezionare alla
|
||||||
|
* lista: il reload di pagina fa ri-renderizzare la sidebar, aggiornando il
|
||||||
|
* badge "Preventivi" nella navigazione.
|
||||||
|
*/
|
||||||
|
class EliminazioneClienteTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <<EOF
|
||||||
|
|
||||||
|
Servizi in avvio:
|
||||||
|
Configuratore http://localhost:5173/?sistema=pvc&type=finestra_2_ante
|
||||||
|
App cliente http://localhost:5199
|
||||||
|
API http://localhost:8000/api
|
||||||
|
Admin http://localhost:8000/admin (admin@kreiosinfissi.it / password)
|
||||||
|
|
||||||
|
Ctrl+C per fermare tutto.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Se uno dei servizi termina, ferma anche gli altri.
|
||||||
|
while true; do
|
||||||
|
for pid in "${PIDS[@]}"; do
|
||||||
|
if ! kill -0 "$pid" 2>/dev/null; then
|
||||||
|
echo "Un servizio si è fermato, arresto degli altri." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
@@ -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** | <http://localhost:5173> |
|
||||||
|
| Backoffice (API + admin) | `backoffice/` | Laravel 13 + Filament 3 + MySQL | **8000** | <http://localhost:8000/api>, admin su <http://localhost:8000/admin> |
|
||||||
|
| App cliente preventivi | `backoffice/cliente/` | Vue 3 + Vite | **5199** | <http://localhost: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: <http://localhost:8000/api>
|
||||||
|
- Pannello admin: <http://localhost:8000/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 `<iframe>` su `kreiosinfissi.it`; in dev si apre direttamente in una tab.
|
||||||
|
|
||||||
|
## Sequenza completa (tre terminali)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminale 1 — backoffice Laravel (API + admin)
|
||||||
|
cd backoffice && php artisan serve --port=8000
|
||||||
|
|
||||||
|
# Terminale 2 — app cliente
|
||||||
|
cd backoffice/cliente && pnpm dev
|
||||||
|
|
||||||
|
# Terminale 3 — configuratore (dalla root del repo)
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifica end-to-end
|
||||||
|
|
||||||
|
1. Apri <http://localhost:5173/?sistema=pvc&type=finestra_2_ante> — il modello 3D si carica e il menu è attivo.
|
||||||
|
2. Configura dimensioni/finiture e clicca **"Aggiungi al preventivo"** → `POST /api/carrello/aggiungi`; il token del carrello viene salvato in `localStorage`.
|
||||||
|
3. Clicca **"Vai al preventivo"** → si apre <http://localhost:5199/?token=XXX> con il riepilogo del carrello.
|
||||||
|
4. Compila i dati del cliente e **conferma**.
|
||||||
|
5. Controlla il risultato:
|
||||||
|
- il preventivo compare in <http://localhost:8000/admin> con stato `inviato`;
|
||||||
|
- le email (cliente con PDF allegato + notifica admin) sono in `backoffice/storage/logs/laravel.log`.
|
||||||
|
|
||||||
|
## Build di produzione (configuratore)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Creare .env.production (non tracciato in git) con l'URL API reale
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
# -> VITE_PREVENTIVI_API_BASE=https://preventivi.kreiosinfissi.it/api
|
||||||
|
|
||||||
|
# 2. Build + zip (build.sh fallisce se .env.production manca)
|
||||||
|
./build.sh # pnpm build -> dist/ -> rinominata in kreios/ + kreios.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
`pnpm preview` serve localmente la build di produzione per un'anteprima.
|
||||||
|
|
||||||
|
## Rigenerare i listini PVC (opzionale)
|
||||||
|
|
||||||
|
`scripts/extract_listini_pvc.py` parsa `PVC_LISTINO.xls` e rigenera i file
|
||||||
|
`src/assets/data/modelli/pvc/<modello>/listino.json`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -r scripts/requirements.txt # xlrd
|
||||||
|
python3 scripts/extract_listini_pvc.py # eseguire dalla root del repo
|
||||||
|
```
|
||||||
|
|
||||||
|
Nota: ogni nuovo titolo presente nel foglio ma non ancora mappato va aggiunto alla costante
|
||||||
|
`KNOWN_FOLDERS` dello script, altrimenti il modello non viene riconosciuto (vedi
|
||||||
|
[ORIGINE_LISTINI_PVC.md](ORIGINE_LISTINI_PVC.md)).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Sintomo | Causa probabile / soluzione |
|
||||||
|
|---|---|
|
||||||
|
| `Port 5173/5199 is already in use` o errore di porta su 8000 | Le porte sono fisse: individuare e fermare l'altro processo (`lsof -i :5173`) |
|
||||||
|
| "Aggiungi al preventivo" dà errore / richieste CORS rifiutate | Laravel non è in ascolto su `8000`, oppure `VITE_PREVENTIVI_API_BASE` nel `.env` della root non punta a `http://localhost:8000/api` |
|
||||||
|
| "Vai al preventivo" apre la URL sbagliata | Controllare `APP_FRONTEND_CLIENTE_URL` nel `.env` del backoffice: il configuratore legge l'URL dell'app cliente da `GET /api/config` |
|
||||||
|
| `SQLSTATE[HY000] [2002] Connection refused` | MySQL non è in esecuzione, oppure credenziali/host errati nel `.env` del backoffice |
|
||||||
|
| `SQLSTATE[HY000] [1049] Unknown database 'kreios_preventivi'` | Creare il database (vedi sezione 1) |
|
||||||
|
| Login admin rifiutato | Eseguire `php artisan migrate --seed` e usare `admin@kreiosinfissi.it` / `password` |
|
||||||
|
| Il configuratore parte con errore a schermo | Manca o è errato uno dei parametri `sistema`/`type` nell'URL |
|
||||||
|
| Prezzi assenti o a zero | Listini non presenti: rigenerarli con lo script Python o verificare i JSON in `src/assets/data/modelli/<sistema>/<modello>/listino.json` |
|
||||||
|
| `php artisan serve` dice che la porta è occupata | Fermare l'altro server: `lsof -i :8000` |
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const Configurator = {
|
|||||||
|
|
||||||
res = Configurator.generateConfigData(res);
|
res = Configurator.generateConfigData(res);
|
||||||
|
|
||||||
$(".prezzo").html(`€ ${res.prezzo.toFixed(2)}<span>IVA Inclusa</span>`);
|
$(".prezzo").html(`€ ${res.prezzo.toFixed(2)}<span>IVA esclusa</span>`);
|
||||||
window.configurazione = res;
|
window.configurazione = res;
|
||||||
|
|
||||||
if($(e.target).data('model')){
|
if($(e.target).data('model')){
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ const PriceCalculator = {
|
|||||||
|
|
||||||
|
|
||||||
prezzo_finale = (prezzo_finale * (1 + percentuale_finiture+percentuale_vetro+data.percentuale_accessori)) + prezzo_accessori;
|
prezzo_finale = (prezzo_finale * (1 + percentuale_finiture+percentuale_vetro+data.percentuale_accessori)) + prezzo_accessori;
|
||||||
prezzo_finale += prezzo_finale * 22 / 100; // IVA 22%
|
// IVA disabilitata: i prezzi sono IVA esclusa
|
||||||
|
// prezzo_finale += prezzo_finale * 22 / 100;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prezzo: prezzo_finale,
|
prezzo: prezzo_finale,
|
||||||
|
|||||||
+21
-1
@@ -23,6 +23,8 @@ window.GLTFLoader = GLTFLoader; // Used by APP Scripts.
|
|||||||
|
|
||||||
// --- Integrazione backend preventivi ---
|
// --- Integrazione backend preventivi ---
|
||||||
const CART_TOKEN_KEY = 'kreios_cart_token';
|
const CART_TOKEN_KEY = 'kreios_cart_token';
|
||||||
|
// Ordine dei sistemi come nel menu: il primo è quello di default
|
||||||
|
const SISTEMI = ['pvc', 'alluminio', 'legno', 'acciaio'];
|
||||||
|
|
||||||
// URL dell'app cliente (checkout): richiesto all'API invece di essere indovinato, perché la
|
// URL dell'app cliente (checkout): richiesto all'API invece di essere indovinato, perché la
|
||||||
// porta Vite dell'app cliente può cambiare (es. se 5199 è già occupata). Fallback locale se
|
// porta Vite dell'app cliente può cambiare (es. se 5199 è già occupata). Fallback locale se
|
||||||
@@ -136,13 +138,31 @@ $(() => {
|
|||||||
const start = async () => {
|
const start = async () => {
|
||||||
const params = ParamsParser.getQueryParams();
|
const params = ParamsParser.getQueryParams();
|
||||||
|
|
||||||
|
// Senza sistema in query string si parte dal primo sistema (pvc)
|
||||||
|
if (!SISTEMI.includes(params.sistema))
|
||||||
|
params.sistema = SISTEMI[0];
|
||||||
|
|
||||||
let data_folder = `./assets/data/sistemi/${params.sistema}`;
|
let data_folder = `./assets/data/sistemi/${params.sistema}`;
|
||||||
let modello_folder = `./assets/data/modelli/${params.sistema}`;
|
let modello_folder = `./assets/data/modelli/${params.sistema}`;
|
||||||
|
|
||||||
|
|
||||||
const modelli = await (await fetch(`${data_folder}/modelli.json?rnd=` + Math.random())).json();
|
const modelli = await (await fetch(`${data_folder}/modelli.json?rnd=` + Math.random())).json();
|
||||||
|
|
||||||
const modello = modelli.find(m => m.id === params.type);
|
let modello = modelli.find(m => m.id === params.type);
|
||||||
|
|
||||||
|
// Senza type (o type non valido) si mostra il primo infisso disponibile
|
||||||
|
if (!modello) {
|
||||||
|
modello = modelli.find(m => m.enabled !== false) || modelli[0];
|
||||||
|
params.type = modello.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allinea la query string ai valori effettivi, così le altre letture (es. carrello) restano coerenti
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (url.searchParams.get('sistema') !== params.sistema || url.searchParams.get('type') !== params.type) {
|
||||||
|
url.searchParams.set('sistema', params.sistema);
|
||||||
|
url.searchParams.set('type', params.type);
|
||||||
|
window.history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await (await fetch(`${modello_folder}/${modello.cartella}/form.json?rnd=${Math.random()}`)).json();
|
const data = await (await fetch(`${modello_folder}/${modello.cartella}/form.json?rnd=${Math.random()}`)).json();
|
||||||
const data_finiture = modello.moduli.includes('modello') ? (await (await fetch(`${modello_folder}/${modello.cartella}/modello.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : [];
|
const data_finiture = modello.moduli.includes('modello') ? (await (await fetch(`${modello_folder}/${modello.cartella}/modello.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : [];
|
||||||
|
|||||||
Reference in New Issue
Block a user