netgescon-day0/docs/91-database/02-tabelle-collegate/stabili.md

23 KiB

🏢 TABELLA STABILI

Edifici e condomini gestiti dall'amministratore


🎯 CARATTERISTICHE TABELLA

Scopo:

Tabella per tutti gli edifici gestiti dall'amministratore - ogni stabile appartiene a un singolo amministratore

Principio:

Isolamento per amministratore - ogni stabile ha amministratore_id per filtro multi-tenant


🗄️ STRUTTURA TABELLA

CREATE TABLE stabili (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    amministratore_id BIGINT UNSIGNED NOT NULL,        -- FK verso amministratori
    
    -- Identificazione stabile
    denominazione VARCHAR(255) NOT NULL,               -- Nome condominio/edificio
    codice_interno VARCHAR(50) NULL,                   -- Codice interno amministratore
    
    -- Indirizzo completo
    indirizzo VARCHAR(255) NOT NULL,
    numero_civico VARCHAR(20) NULL,
    scala VARCHAR(50) NULL,
    cap VARCHAR(10) NOT NULL,
    comune_id INT UNSIGNED NULL,                       -- FK verso comuni_italiani
    provincia VARCHAR(2) NOT NULL,
    regione VARCHAR(50) NULL,
    
    -- Dati catastali
    foglio VARCHAR(20) NULL,
    particella VARCHAR(20) NULL,
    subalterno VARCHAR(20) NULL,
    categoria_catastale VARCHAR(10) NULL,
    classe VARCHAR(10) NULL,
    consistenza VARCHAR(50) NULL,
    rendita_catastale DECIMAL(10, 2) NULL,
    
    -- Caratteristiche edificio
    anno_costruzione YEAR NULL,
    numero_piani INT NULL,
    numero_unita INT DEFAULT 0,                        -- Calcolato automaticamente
    ascensore BOOLEAN DEFAULT FALSE,
    riscaldamento_centralizzato BOOLEAN DEFAULT FALSE,
    tipo_edificio ENUM('condominio', 'villetta', 'complesso', 'altro') DEFAULT 'condominio',
    
    -- Dati amministrativi
    codice_fiscale_condominio VARCHAR(16) NULL,        -- CF del condominio
    partita_iva_condominio VARCHAR(11) NULL,
    data_nomina_amministratore DATE NULL,
    data_scadenza_mandato DATE NULL,
    periodicita_assemblea ENUM('annuale', 'semestrale') DEFAULT 'annuale',
    
    -- Configurazioni
    usa_millesimi BOOLEAN DEFAULT TRUE,
    valuta_default VARCHAR(3) DEFAULT 'EUR',
    configurazioni JSON NULL,                           -- Config specifiche stabile
    
    -- Stato
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    
    -- Audit
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (amministratore_id) REFERENCES amministratori(id) ON DELETE CASCADE,
    FOREIGN KEY (comune_id) REFERENCES comuni_italiani(id) ON DELETE SET NULL,
    
    -- Indici
    INDEX idx_amministratore_id (amministratore_id),
    INDEX idx_comune_id (comune_id),
    INDEX idx_denominazione (denominazione),
    INDEX idx_active (is_active),
    INDEX idx_cap (cap),
    INDEX idx_provincia (provincia),
    INDEX idx_tipo_edificio (tipo_edificio),
    
    -- Indice composto per ricerche
    INDEX idx_admin_denominazione (amministratore_id, denominazione),
    INDEX idx_admin_active (amministratore_id, is_active)
);

🏗️ MIGRATION LARAVEL

File: database/migrations/2024_08_05_000010_create_stabili_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('stabili', function (Blueprint $table) {
            $table->id();
            $table->foreignId('amministratore_id')
                  ->constrained('amministratori')
                  ->onDelete('cascade');
                  
            // Identificazione stabile
            $table->string('denominazione');
            $table->string('codice_interno', 50)->nullable();
            
            // Indirizzo completo
            $table->string('indirizzo');
            $table->string('numero_civico', 20)->nullable();
            $table->string('scala', 50)->nullable();
            $table->string('cap', 10);
            $table->unsignedInteger('comune_id')->nullable();
            $table->string('provincia', 2);
            $table->string('regione', 50)->nullable();
            
            // Dati catastali
            $table->string('foglio', 20)->nullable();
            $table->string('particella', 20)->nullable();
            $table->string('subalterno', 20)->nullable();
            $table->string('categoria_catastale', 10)->nullable();
            $table->string('classe', 10)->nullable();
            $table->string('consistenza', 50)->nullable();
            $table->decimal('rendita_catastale', 10, 2)->nullable();
            
            // Caratteristiche edificio
            $table->year('anno_costruzione')->nullable();
            $table->integer('numero_piani')->nullable();
            $table->integer('numero_unita')->default(0);
            $table->boolean('ascensore')->default(false);
            $table->boolean('riscaldamento_centralizzato')->default(false);
            $table->enum('tipo_edificio', ['condominio', 'villetta', 'complesso', 'altro'])
                  ->default('condominio');
            
            // Dati amministrativi
            $table->string('codice_fiscale_condominio', 16)->nullable();
            $table->string('partita_iva_condominio', 11)->nullable();
            $table->date('data_nomina_amministratore')->nullable();
            $table->date('data_scadenza_mandato')->nullable();
            $table->enum('periodicita_assemblea', ['annuale', 'semestrale'])
                  ->default('annuale');
            
            // Configurazioni
            $table->boolean('usa_millesimi')->default(true);
            $table->string('valuta_default', 3)->default('EUR');
            $table->json('configurazioni')->nullable();
            
            // Stato
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            
            $table->timestamps();
            
            // Foreign Keys
            $table->foreign('comune_id')
                  ->references('id')
                  ->on('comuni_italiani')
                  ->onDelete('set null');
            
            // Indici
            $table->index(['amministratore_id']);
            $table->index(['comune_id']);
            $table->index(['denominazione']);
            $table->index(['is_active']);
            $table->index(['cap']);
            $table->index(['provincia']);
            $table->index(['tipo_edificio']);
            
            // Indici composti
            $table->index(['amministratore_id', 'denominazione'], 'idx_admin_denominazione');
            $table->index(['amministratore_id', 'is_active'], 'idx_admin_active');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('stabili');
    }
};

📊 MODEL ELOQUENT

File: app/Models/Stabile.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;

class Stabile extends Model
{
    protected $fillable = [
        'amministratore_id',
        'denominazione',
        'codice_interno',
        'indirizzo',
        'numero_civico',
        'scala',
        'cap',
        'comune_id',
        'provincia',
        'regione',
        'foglio',
        'particella',
        'subalterno',
        'categoria_catastale',
        'classe',
        'consistenza',
        'rendita_catastale',
        'anno_costruzione',
        'numero_piani',
        'ascensore',
        'riscaldamento_centralizzato',
        'tipo_edificio',
        'codice_fiscale_condominio',
        'partita_iva_condominio',
        'data_nomina_amministratore',
        'data_scadenza_mandato',
        'periodicita_assemblea',
        'usa_millesimi',
        'valuta_default',
        'configurazioni',
        'is_active',
        'note',
    ];

    protected $casts = [
        'ascensore' => 'boolean',
        'riscaldamento_centralizzato' => 'boolean',
        'usa_millesimi' => 'boolean',
        'is_active' => 'boolean',
        'data_nomina_amministratore' => 'date',
        'data_scadenza_mandato' => 'date',
        'rendita_catastale' => 'decimal:2',
        'configurazioni' => 'array',
        'numero_piani' => 'integer',
        'numero_unita' => 'integer',
    ];

    // Relazioni
    public function amministratore(): BelongsTo
    {
        return $this->belongsTo(Amministratore::class);
    }

    public function comune(): BelongsTo
    {
        return $this->belongsTo(ComuneItaliano::class, 'comune_id');
    }

    public function unitaImmobiliari(): HasMany
    {
        return $this->hasMany(UnitaImmobiliare::class);
    }

    public function gestioni(): HasMany
    {
        return $this->hasMany(Gestione::class);
    }

    public function documenti(): HasMany
    {
        return $this->hasMany(Documento::class);
    }

    public function soggetti(): HasMany
    {
        return $this->hasMany(Soggetto::class);
    }

    // Scope automatico per amministratore corrente
    protected static function booted(): void
    {
        static::addGlobalScope('amministratore', function (Builder $builder) {
            $user = auth()->user();
            if ($user && $user->amministratore_id) {
                $builder->where('amministratore_id', $user->amministratore_id);
            }
        });
    }

    // Scope
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }

    public function scopeByTipo(Builder $query, string $tipo): Builder
    {
        return $query->where('tipo_edificio', $tipo);
    }

    public function scopeByProvincia(Builder $query, string $provincia): Builder
    {
        return $query->where('provincia', $provincia);
    }

    public function scopeConAscensore(Builder $query): Builder
    {
        return $query->where('ascensore', true);
    }

    public function scopeConRiscaldamentoCentralizzato(Builder $query): Builder
    {
        return $query->where('riscaldamento_centralizzato', true);
    }

    // Accessors
    public function getIndirizzoCompletoAttribute(): string
    {
        $parts = array_filter([
            $this->indirizzo,
            $this->numero_civico,
            $this->scala ? "Scala {$this->scala}" : null,
        ]);
        
        return implode(', ', $parts);
    }

    public function getLocalitaAttribute(): string
    {
        $parts = array_filter([
            $this->cap,
            $this->comune?->denominazione,
            $this->provincia ? "({$this->provincia})" : null,
        ]);
        
        return implode(' ', $parts);
    }

    public function getIndirizzoFormattato(): string
    {
        return "{$this->indirizzo_completo}\n{$this->localita}";
    }

    public function getMandatoScadutoAttribute(): bool
    {
        return $this->data_scadenza_mandato && 
               $this->data_scadenza_mandato->isPast();
    }

    public function getGiorniAllaScadenzaAttribute(): ?int
    {
        if (!$this->data_scadenza_mandato) {
            return null;
        }
        
        return now()->diffInDays($this->data_scadenza_mandato, false);
    }

    // Helper Methods
    public function updateNumeroUnita(): void
    {
        $count = $this->unitaImmobiliari()->count();
        $this->update(['numero_unita' => $count]);
    }

    public function getGestioneAttiva(): ?Gestione
    {
        return $this->gestioni()
            ->where('anno_gestione', now()->year)
            ->where('is_active', true)
            ->first();
    }

    public function hasGestioneAttiva(): bool
    {
        return $this->getGestioneAttiva() !== null;
    }

    public function canDelete(): bool
    {
        return $this->unitaImmobiliari()->count() === 0 &&
               $this->gestioni()->count() === 0;
    }

    public function getStatistiche(): array
    {
        return [
            'numero_unita' => $this->numero_unita,
            'gestioni_totali' => $this->gestioni()->count(),
            'gestioni_attive' => $this->gestioni()->where('is_active', true)->count(),
            'documenti_totali' => $this->documenti()->count(),
            'soggetti_coinvolti' => $this->soggetti()->count(),
        ];
    }

    // Observer Events
    protected static function boot(): void
    {
        parent::boot();

        static::creating(function ($stabile) {
            // Assegna automaticamente l'amministratore corrente se non specificato
            if (!$stabile->amministratore_id && auth()->check()) {
                $stabile->amministratore_id = auth()->user()->amministratore_id;
            }
        });

        static::saved(function ($stabile) {
            // Aggiorna conteggio unità dopo salvataggio
            $stabile->updateNumeroUnita();
        });
    }
}

🌱 SEEDER DATI DEMO

File: database/seeders/StabiliSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Stabile;
use App\Models\Amministratore;
use App\Models\ComuneItaliano;

class StabiliSeeder extends Seeder
{
    public function run(): void
    {
        $amministratori = Amministratore::all();
        
        // Trova alcuni comuni per i test
        $milano = ComuneItaliano::where('denominazione', 'Milano')->first();
        $roma = ComuneItaliano::where('denominazione', 'Roma')->first();
        $torino = ComuneItaliano::where('denominazione', 'Torino')->first();

        foreach ($amministratori as $admin) {
            // Stabile 1 - Condominio grande
            Stabile::create([
                'amministratore_id' => $admin->id,
                'denominazione' => 'Condominio Bella Vista',
                'codice_interno' => 'BV001',
                'indirizzo' => 'Via Roma',
                'numero_civico' => '123',
                'cap' => $admin->id === 1 ? '20100' : ($admin->id === 2 ? '00100' : '10100'),
                'comune_id' => $admin->id === 1 ? $milano?->id : ($admin->id === 2 ? $roma?->id : $torino?->id),
                'provincia' => $admin->id === 1 ? 'MI' : ($admin->id === 2 ? 'RM' : 'TO'),
                'regione' => $admin->id === 1 ? 'Lombardia' : ($admin->id === 2 ? 'Lazio' : 'Piemonte'),
                'foglio' => '123',
                'particella' => '456',
                'categoria_catastale' => 'A/2',
                'classe' => '3',
                'rendita_catastale' => 15000.00,
                'anno_costruzione' => 1985,
                'numero_piani' => 5,
                'ascensore' => true,
                'riscaldamento_centralizzato' => true,
                'tipo_edificio' => 'condominio',
                'data_nomina_amministratore' => now()->subMonths(6),
                'data_scadenza_mandato' => now()->addMonths(6),
                'periodicita_assemblea' => 'annuale',
                'usa_millesimi' => true,
                'is_active' => true,
            ]);

            // Stabile 2 - Villetta
            Stabile::create([
                'amministratore_id' => $admin->id,
                'denominazione' => 'Villa dei Fiori',
                'codice_interno' => 'VF001',
                'indirizzo' => 'Via Dante Alighieri',
                'numero_civico' => '45',
                'cap' => $admin->id === 1 ? '20100' : ($admin->id === 2 ? '00100' : '10100'),
                'comune_id' => $admin->id === 1 ? $milano?->id : ($admin->id === 2 ? $roma?->id : $torino?->id),
                'provincia' => $admin->id === 1 ? 'MI' : ($admin->id === 2 ? 'RM' : 'TO'),
                'regione' => $admin->id === 1 ? 'Lombardia' : ($admin->id === 2 ? 'Lazio' : 'Piemonte'),
                'foglio' => '789',
                'particella' => '012',
                'categoria_catastale' => 'A/7',
                'classe' => '4',
                'rendita_catastale' => 8000.00,
                'anno_costruzione' => 2000,
                'numero_piani' => 2,
                'ascensore' => false,
                'riscaldamento_centralizzato' => false,
                'tipo_edificio' => 'villetta',
                'data_nomina_amministratore' => now()->subYear(),
                'data_scadenza_mandato' => now()->addYear(),
                'periodicita_assemblea' => 'annuale',
                'usa_millesimi' => false,
                'is_active' => true,
            ]);

            // Stabile 3 - Complesso residenziale
            if ($admin->piano_abbonamento !== 'base') {
                Stabile::create([
                    'amministratore_id' => $admin->id,
                    'denominazione' => 'Residenza Park View',
                    'codice_interno' => 'PV001',
                    'indirizzo' => 'Viale Europa',
                    'numero_civico' => '200',
                    'scala' => 'A-B-C',
                    'cap' => $admin->id === 1 ? '20100' : ($admin->id === 2 ? '00100' : '10100'),
                    'comune_id' => $admin->id === 1 ? $milano?->id : ($admin->id === 2 ? $roma?->id : $torino?->id),
                    'provincia' => $admin->id === 1 ? 'MI' : ($admin->id === 2 ? 'RM' : 'TO'),
                    'regione' => $admin->id === 1 ? 'Lombardia' : ($admin->id === 2 ? 'Lazio' : 'Piemonte'),
                    'anno_costruzione' => 2010,
                    'numero_piani' => 8,
                    'ascensore' => true,
                    'riscaldamento_centralizzato' => true,
                    'tipo_edificio' => 'complesso',
                    'codice_fiscale_condominio' => '80123456789',
                    'data_nomina_amministratore' => now()->subMonths(3),
                    'data_scadenza_mandato' => now()->addMonths(9),
                    'periodicita_assemblea' => 'annuale',
                    'usa_millesimi' => true,
                    'configurazioni' => [
                        'portierato' => true,
                        'videosorveglianza' => true,
                        'piscina' => true,
                        'palestra' => false,
                    ],
                    'is_active' => true,
                ]);
            }
        }

        $totalStabili = Stabile::count();
        $this->command->info("✅ Creati {$totalStabili} stabili per tutti gli amministratori");
        
        // Mostra riassunto per amministratore
        foreach ($amministratori as $admin) {
            $count = $admin->stabili()->count();
            $this->command->info("Admin {$admin->codice_univoco}: {$count} stabili");
        }
    }
}

🔍 QUERY PATTERNS

Query Base per Amministratore:

// Controller - Lista stabili dell'amministratore corrente
public function index()
{
    $stabili = Stabile::with(['comune', 'unitaImmobiliari'])
        ->active()
        ->orderBy('denominazione')
        ->paginate(20);
        
    return view('stabili.index', compact('stabili'));
}

// Query con filtri
public function search(Request $request)
{
    $query = Stabile::query();
    
    if ($request->filled('denominazione')) {
        $query->where('denominazione', 'like', "%{$request->denominazione}%");
    }
    
    if ($request->filled('provincia')) {
        $query->where('provincia', $request->provincia);
    }
    
    if ($request->filled('tipo_edificio')) {
        $query->byTipo($request->tipo_edificio);
    }
    
    $stabili = $query->with('comune')->paginate(20);
    
    return view('stabili.index', compact('stabili'));
}

Statistiche per Dashboard:

// Controller Dashboard
public function dashboard()
{
    $amministratore = auth()->user()->amministratore;
    
    $stats = [
        'stabili_totali' => $amministratore->stabili()->count(),
        'stabili_attivi' => $amministratore->stabili()->active()->count(),
        'unita_totali' => $amministratore->stabili()
            ->withCount('unitaImmobiliari')
            ->get()
            ->sum('unita_immobiliari_count'),
        'mandati_in_scadenza' => $amministratore->stabili()
            ->whereDate('data_scadenza_mandato', '<=', now()->addDays(30))
            ->count(),
    ];
    
    return view('dashboard', compact('stats'));
}

🎛️ FORM VALIDATION

File: app/Http/Requests/StabileRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StabileRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->check();
    }

    public function rules(): array
    {
        $stabileId = $this->route('stabile')?->id;
        
        return [
            'denominazione' => 'required|string|max:255',
            'codice_interno' => [
                'nullable',
                'string',
                'max:50',
                Rule::unique('stabili')
                    ->where('amministratore_id', auth()->user()->amministratore_id)
                    ->ignore($stabileId)
            ],
            'indirizzo' => 'required|string|max:255',
            'numero_civico' => 'nullable|string|max:20',
            'cap' => 'required|string|size:5',
            'provincia' => 'required|string|size:2',
            'comune_id' => 'nullable|exists:comuni_italiani,id',
            'tipo_edificio' => 'required|in:condominio,villetta,complesso,altro',
            'anno_costruzione' => 'nullable|integer|min:1800|max:' . (date('Y') + 5),
            'numero_piani' => 'nullable|integer|min:1|max:100',
            'ascensore' => 'boolean',
            'riscaldamento_centralizzato' => 'boolean',
            'data_nomina_amministratore' => 'nullable|date',
            'data_scadenza_mandato' => 'nullable|date|after:data_nomina_amministratore',
            'codice_fiscale_condominio' => 'nullable|string|size:16',
            'partita_iva_condominio' => 'nullable|string|size:11',
            'rendita_catastale' => 'nullable|numeric|min:0',
        ];
    }

    public function messages(): array
    {
        return [
            'denominazione.required' => 'La denominazione è obbligatoria.',
            'indirizzo.required' => 'L\'indirizzo è obbligatorio.',
            'cap.required' => 'Il CAP è obbligatorio.',
            'cap.size' => 'Il CAP deve essere di 5 cifre.',
            'provincia.required' => 'La provincia è obbligatoria.',
            'codice_interno.unique' => 'Questo codice interno è già utilizzato per un altro stabile.',
            'data_scadenza_mandato.after' => 'La data scadenza deve essere successiva alla data nomina.',
        ];
    }
}

🎯 UTILIZZO PATTERN

Nel Controller:

// Sempre filtrato per amministratore corrente grazie al Global Scope
$stabili = Stabile::all(); // Solo stabili dell'amministratore loggato

// Query specifiche
$condomini = Stabile::byTipo('condominio')->get();
$stabiliMilano = Stabile::byProvincia('MI')->get();
$conAscensore = Stabile::conAscensore()->get();

Nei Blade Templates:

{{-- Lista stabili --}}
@foreach($stabili as $stabile)
    <div class="card">
        <h3>{{ $stabile->denominazione }}</h3>
        <p>{{ $stabile->indirizzo_formattato }}</p>
        <span class="badge">{{ $stabile->numero_unita }} unità</span>
        
        @if($stabile->mandato_scaduto)
            <span class="badge badge-danger">Mandato scaduto</span>
        @elseif($stabile->giorni_alla_scadenza <= 30)
            <span class="badge badge-warning">Scade tra {{ $stabile->giorni_alla_scadenza }} giorni</span>
        @endif
    </div>
@endforeach

🎯 TABELLA STABILI DEFINITA
Con isolamento per amministratore, dati catastali completi e gestione mandati

📖 Prossimo: gestioni.md per collegare i periodi amministrativi agli stabili