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

28 KiB
Executable File

🏠 TABELLA UNITA IMMOBILIARI

Appartamenti, locali e box collegati agli stabili


🔑 CONNESSIONE DATABASE

Credenziali NetGescon:

# Connessione MySQL
mysql -u netgescon_user -p netgescon
# Password: NetGescon2024!

# Test connessione Laravel
cd /var/www/netgescon
php artisan tinker
DB::connection()->getPdo();

🎯 CARATTERISTICHE TABELLA

Scopo:

Tabella per tutti gli appartamenti, locali, box e cantine appartenenti agli stabili

Principio:

Collegamento via stabile - ogni unità appartiene a uno stabile → isolamento automatico per amministratore


🗄️ STRUTTURA TABELLA

CREATE TABLE unita_immobiliari (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stabile_id BIGINT UNSIGNED NOT NULL,              -- FK verso stabili
    
    -- Identificazione unità
    interno VARCHAR(20) NOT NULL,                     -- A1, B12, Box 5, ecc
    piano INT NULL,                                   -- Piano (0=terra, -1=seminterrato)
    scala VARCHAR(10) NULL,                           -- A, B, C
    denominazione VARCHAR(255) NULL,                  -- Appartamento al 2° piano
    codice_interno VARCHAR(50) NULL,                  -- Codice amministratore
    
    -- Tipologia unità
    tipo_unita ENUM('appartamento', 'locale_commerciale', 'box', 'cantina', 'mansarda', 'soffitta', 'altro') DEFAULT 'appartamento',
    destinazione_uso ENUM('abitazione', 'commerciale', 'ufficio', 'deposito', 'altro') DEFAULT 'abitazione',
    
    -- Dati catastali
    foglio VARCHAR(20) NULL,
    particella VARCHAR(20) NULL,
    subalterno VARCHAR(20) NOT NULL,                  -- Obbligatorio per unità
    categoria_catastale VARCHAR(10) NULL,             -- A/2, A/3, C/1, ecc
    classe VARCHAR(10) NULL,
    consistenza VARCHAR(50) NULL,                     -- Vani, mq, mc
    superficie_commerciale DECIMAL(8, 2) NULL,       -- mq
    superficie_calpestabile DECIMAL(8, 2) NULL,      -- mq
    rendita_catastale DECIMAL(8, 2) NULL,            -- Euro
    
    -- Caratteristiche fisiche
    numero_vani INT NULL,
    numero_bagni INT NULL,
    numero_balconi INT NULL,
    has_terrazzo BOOLEAN DEFAULT FALSE,
    has_giardino BOOLEAN DEFAULT FALSE,
    has_cantina BOOLEAN DEFAULT FALSE,
    has_box BOOLEAN DEFAULT FALSE,
    
    -- Millesimi
    millesimi_generali DECIMAL(8, 4) DEFAULT 0.0000, -- /1000
    millesimi_riscaldamento DECIMAL(8, 4) DEFAULT 0.0000,
    millesimi_ascensore DECIMAL(8, 4) DEFAULT 0.0000,
    millesimi_scala DECIMAL(8, 4) DEFAULT 0.0000,
    millesimi_personalizzati JSON NULL,              -- Altri tipi millesimi
    
    -- Stato occupazione
    stato_occupazione ENUM('libera', 'occupata_proprietario', 'occupata_inquilino', 'sfitto', 'vendita') DEFAULT 'libera',
    data_ultimo_cambio_stato DATE NULL,
    
    -- Configurazioni
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    configurazioni JSON NULL,                        -- Config specifiche unità
    
    -- Audit
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    
    -- Indici
    INDEX idx_stabile_id (stabile_id),
    INDEX idx_interno (interno),
    INDEX idx_piano (piano),
    INDEX idx_tipo_unita (tipo_unita),
    INDEX idx_categoria_catastale (categoria_catastale),
    INDEX idx_stato_occupazione (stato_occupazione),
    INDEX idx_active (is_active),
    INDEX idx_subalterno (subalterno),
    
    -- Indici composti
    INDEX idx_stabile_interno (stabile_id, interno),
    INDEX idx_stabile_piano (stabile_id, piano),
    INDEX idx_catastale (foglio, particella, subalterno),
    
    -- Vincolo unicità: interno univoco per stabile
    UNIQUE KEY unique_stabile_interno (stabile_id, interno),
    -- Vincolo unicità: subalterno univoco per stabile
    UNIQUE KEY unique_stabile_subalterno (stabile_id, subalterno)
);

🏗️ MIGRATION LARAVEL

File: database/migrations/2024_08_05_000030_create_unita_immobiliari_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('unita_immobiliari', function (Blueprint $table) {
            $table->id();
            $table->foreignId('stabile_id')
                  ->constrained('stabili')
                  ->onDelete('cascade');
                  
            // Identificazione unità
            $table->string('interno', 20);
            $table->integer('piano')->nullable();
            $table->string('scala', 10)->nullable();
            $table->string('denominazione')->nullable();
            $table->string('codice_interno', 50)->nullable();
            
            // Tipologia unità
            $table->enum('tipo_unita', [
                'appartamento', 'locale_commerciale', 'box', 
                'cantina', 'mansarda', 'soffitta', 'altro'
            ])->default('appartamento');
            $table->enum('destinazione_uso', [
                'abitazione', 'commerciale', 'ufficio', 'deposito', 'altro'
            ])->default('abitazione');
            
            // Dati catastali
            $table->string('foglio', 20)->nullable();
            $table->string('particella', 20)->nullable();
            $table->string('subalterno', 20);
            $table->string('categoria_catastale', 10)->nullable();
            $table->string('classe', 10)->nullable();
            $table->string('consistenza', 50)->nullable();
            $table->decimal('superficie_commerciale', 8, 2)->nullable();
            $table->decimal('superficie_calpestabile', 8, 2)->nullable();
            $table->decimal('rendita_catastale', 8, 2)->nullable();
            
            // Caratteristiche fisiche
            $table->integer('numero_vani')->nullable();
            $table->integer('numero_bagni')->nullable();
            $table->integer('numero_balconi')->nullable();
            $table->boolean('has_terrazzo')->default(false);
            $table->boolean('has_giardino')->default(false);
            $table->boolean('has_cantina')->default(false);
            $table->boolean('has_box')->default(false);
            
            // Millesimi
            $table->decimal('millesimi_generali', 8, 4)->default(0.0000);
            $table->decimal('millesimi_riscaldamento', 8, 4)->default(0.0000);
            $table->decimal('millesimi_ascensore', 8, 4)->default(0.0000);
            $table->decimal('millesimi_scala', 8, 4)->default(0.0000);
            $table->json('millesimi_personalizzati')->nullable();
            
            // Stato occupazione
            $table->enum('stato_occupazione', [
                'libera', 'occupata_proprietario', 'occupata_inquilino', 'sfitto', 'vendita'
            ])->default('libera');
            $table->date('data_ultimo_cambio_stato')->nullable();
            
            // Configurazioni
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            $table->json('configurazioni')->nullable();
            
            $table->timestamps();
            
            // Indici
            $table->index(['stabile_id']);
            $table->index(['interno']);
            $table->index(['piano']);
            $table->index(['tipo_unita']);
            $table->index(['categoria_catastale']);
            $table->index(['stato_occupazione']);
            $table->index(['is_active']);
            $table->index(['subalterno']);
            
            // Indici composti
            $table->index(['stabile_id', 'interno'], 'idx_stabile_interno');
            $table->index(['stabile_id', 'piano'], 'idx_stabile_piano');
            $table->index(['foglio', 'particella', 'subalterno'], 'idx_catastale');
            
            // Vincoli unicità
            $table->unique(['stabile_id', 'interno'], 'unique_stabile_interno');
            $table->unique(['stabile_id', 'subalterno'], 'unique_stabile_subalterno');
        });
    }

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

📊 MODEL ELOQUENT

File: app/Models/UnitaImmobiliare.php

<?php

namespace App\Models;

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

class UnitaImmobiliare extends Model
{
    protected $fillable = [
        'stabile_id',
        'interno',
        'piano',
        'scala',
        'denominazione',
        'codice_interno',
        'tipo_unita',
        'destinazione_uso',
        'foglio',
        'particella',
        'subalterno',
        'categoria_catastale',
        'classe',
        'consistenza',
        'superficie_commerciale',
        'superficie_calpestabile',
        'rendita_catastale',
        'numero_vani',
        'numero_bagni',
        'numero_balconi',
        'has_terrazzo',
        'has_giardino',
        'has_cantina',
        'has_box',
        'millesimi_generali',
        'millesimi_riscaldamento',
        'millesimi_ascensore',
        'millesimi_scala',
        'millesimi_personalizzati',
        'stato_occupazione',
        'data_ultimo_cambio_stato',
        'is_active',
        'note',
        'configurazioni',
    ];

    protected $casts = [
        'piano' => 'integer',
        'superficie_commerciale' => 'decimal:2',
        'superficie_calpestabile' => 'decimal:2',
        'rendita_catastale' => 'decimal:2',
        'numero_vani' => 'integer',
        'numero_bagni' => 'integer',
        'numero_balconi' => 'integer',
        'has_terrazzo' => 'boolean',
        'has_giardino' => 'boolean',
        'has_cantina' => 'boolean',
        'has_box' => 'boolean',
        'millesimi_generali' => 'decimal:4',
        'millesimi_riscaldamento' => 'decimal:4',
        'millesimi_ascensore' => 'decimal:4',
        'millesimi_scala' => 'decimal:4',
        'millesimi_personalizzati' => 'array',
        'data_ultimo_cambio_stato' => 'date',
        'is_active' => 'boolean',
        'configurazioni' => 'array',
    ];

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

    public function soggetti(): BelongsToMany
    {
        return $this->belongsToMany(Soggetto::class, 'unita_soggetti')
                    ->withPivot([
                        'tipo_rapporto',
                        'quota_proprieta',
                        'data_inizio',
                        'data_fine',
                        'is_active'
                    ])
                    ->withTimestamps();
    }

    public function proprietari(): BelongsToMany
    {
        return $this->soggetti()->wherePivot('tipo_rapporto', 'proprietario');
    }

    public function inquilini(): BelongsToMany
    {
        return $this->soggetti()->wherePivot('tipo_rapporto', 'inquilino');
    }

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

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

    // Scope automatico per amministratore tramite stabile
    protected static function booted(): void
    {
        static::addGlobalScope('amministratore', function (Builder $builder) {
            $user = auth()->user();
            if ($user && $user->amministratore_id) {
                $builder->whereHas('stabile', function ($query) use ($user) {
                    $query->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_unita', $tipo);
    }

    public function scopeByPiano(Builder $query, int $piano): Builder
    {
        return $query->where('piano', $piano);
    }

    public function scopeByStato(Builder $query, string $stato): Builder
    {
        return $query->where('stato_occupazione', $stato);
    }

    public function scopeLibere(Builder $query): Builder
    {
        return $query->where('stato_occupazione', 'libera');
    }

    public function scopeOccupate(Builder $query): Builder
    {
        return $query->whereIn('stato_occupazione', ['occupata_proprietario', 'occupata_inquilino']);
    }

    public function scopeByMillesimi(Builder $query, string $tipo = 'generali'): Builder
    {
        $campo = "millesimi_{$tipo}";
        return $query->where($campo, '>', 0);
    }

    // Accessors
    public function getDenominazioneComputedAttribute(): string
    {
        return $this->denominazione ?: "{$this->tipo_unita} {$this->interno}";
    }

    public function getIndirizzoCompletoAttribute(): string
    {
        $parts = array_filter([
            $this->stabile->denominazione,
            $this->scala ? "Scala {$this->scala}" : null,
            "Interno {$this->interno}",
            $this->piano !== null ? "Piano {$this->piano}" : null,
        ]);
        
        return implode(', ', $parts);
    }

    public function getIdentificativoCatastaleAttribute(): string
    {
        $parts = array_filter([
            $this->foglio ? "F.{$this->foglio}" : null,
            $this->particella ? "P.{$this->particella}" : null,
            $this->subalterno ? "Sub.{$this->subalterno}" : null,
        ]);
        
        return implode(' ', $parts);
    }

    public function getSuperficieTotaleAttribute(): ?float
    {
        return $this->superficie_commerciale ?: $this->superficie_calpestabile;
    }

    public function getIsAppartamentoAttribute(): bool
    {
        return $this->tipo_unita === 'appartamento';
    }

    public function getIsLocaleCommercialeAttribute(): bool
    {
        return $this->tipo_unita === 'locale_commerciale';
    }

    public function getIsBoxAttribute(): bool
    {
        return $this->tipo_unita === 'box';
    }

    public function getHasProprietarioAttribute(): bool
    {
        return $this->proprietari()->exists();
    }

    public function getHasInquilinoAttribute(): bool
    {
        return $this->inquilini()->exists();
    }

    public function getStatoColorAttribute(): string
    {
        return match($this->stato_occupazione) {
            'libera' => 'gray',
            'occupata_proprietario' => 'green',
            'occupata_inquilino' => 'blue',
            'sfitto' => 'yellow',
            'vendita' => 'red',
            default => 'gray'
        };
    }

    // Helper Methods
    public function calcolaMillesimiAutomatici(): void
    {
        if (!$this->superficie_totale) {
            return;
        }
        
        $stabileSuperficieTotale = $this->stabile->unitaImmobiliari()
            ->whereNotNull('superficie_commerciale')
            ->sum('superficie_commerciale');
            
        if ($stabileSuperficieTotale > 0) {
            $millesimi = ($this->superficie_totale / $stabileSuperficieTotale) * 1000;
            
            $this->update([
                'millesimi_generali' => round($millesimi, 4)
            ]);
        }
    }

    public function aggiornaStatoOccupazione(string $nuovoStato): bool
    {
        if ($this->stato_occupazione === $nuovoStato) {
            return false;
        }
        
        $this->update([
            'stato_occupazione' => $nuovoStato,
            'data_ultimo_cambio_stato' => now()->toDateString()
        ]);
        
        return true;
    }

    public function getProprietarioAttuale(): ?Soggetto
    {
        return $this->proprietari()
            ->wherePivot('is_active', true)
            ->wherePivot('data_fine', null)
            ->first();
    }

    public function getInquilinoAttuale(): ?Soggetto
    {
        return $this->inquilini()
            ->wherePivot('is_active', true)
            ->wherePivot('data_fine', null)
            ->first();
    }

    public function associaSoggetto(Soggetto $soggetto, string $tipoRapporto, array $datiAggiuntivi = []): void
    {
        $datiPivot = array_merge([
            'tipo_rapporto' => $tipoRapporto,
            'quota_proprieta' => $datiAggiuntivi['quota_proprieta'] ?? 100.00,
            'data_inizio' => $datiAggiuntivi['data_inizio'] ?? now()->toDateString(),
            'data_fine' => $datiAggiuntivi['data_fine'] ?? null,
            'is_active' => $datiAggiuntivi['is_active'] ?? true,
        ], $datiAggiuntivi);
        
        $this->soggetti()->attach($soggetto->id, $datiPivot);
        
        // Aggiorna stato occupazione automaticamente
        if ($tipoRapporto === 'proprietario') {
            $this->aggiornaStatoOccupazione('occupata_proprietario');
        } elseif ($tipoRapporto === 'inquilino') {
            $this->aggiornaStatoOccupazione('occupata_inquilino');
        }
    }

    public function rimuoviSoggetto(Soggetto $soggetto): void
    {
        $this->soggetti()->updateExistingPivot($soggetto->id, [
            'is_active' => false,
            'data_fine' => now()->toDateString()
        ]);
        
        // Controlla se rimangono soggetti attivi
        $hasProprietarioAttivo = $this->proprietari()
            ->wherePivot('is_active', true)
            ->exists();
            
        $hasInquilinoAttivo = $this->inquilini()
            ->wherePivot('is_active', true)
            ->exists();
        
        if (!$hasProprietarioAttivo && !$hasInquilinoAttivo) {
            $this->aggiornaStatoOccupazione('libera');
        }
    }

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

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

        static::creating(function ($unita) {
            // Genera denominazione automatica se non specificata
            if (!$unita->denominazione) {
                $unita->denominazione = ucfirst($unita->tipo_unita) . " {$unita->interno}";
            }
        });

        static::saved(function ($unita) {
            // Aggiorna conteggio unità dello stabile
            $unita->stabile->updateNumeroUnita();
            
            // Ricalcola millesimi se superficie cambiata
            if ($unita->wasChanged('superficie_commerciale')) {
                $unita->calcolaMillesimiAutomatici();
            }
        });

        static::deleted(function ($unita) {
            // Aggiorna conteggio unità dello stabile
            $unita->stabile->updateNumeroUnita();
        });
    }
}

🌱 SEEDER DATI DEMO

File: database/seeders/UnitaImmobiliariSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\UnitaImmobiliare;
use App\Models\Stabile;

class UnitaImmobiliariSeeder extends Seeder
{
    public function run(): void
    {
        $stabili = Stabile::all();
        
        foreach ($stabili as $stabile) {
            $this->createUnitaPerStabile($stabile);
        }

        $totalUnita = UnitaImmobiliare::count();
        $this->command->info("✅ Create {$totalUnita} unità immobiliari per tutti gli stabili");
        
        // Mostra riassunto per amministratore
        $stabili->groupBy('amministratore_id')->each(function ($stabiliGroup) {
            $admin = $stabiliGroup->first()->amministratore;
            $unitaCount = UnitaImmobiliare::whereIn('stabile_id', $stabiliGroup->pluck('id'))->count();
            $this->command->info("Admin {$admin->codice_univoco}: {$unitaCount} unità immobiliari");
        });
    }
    
    private function createUnitaPerStabile(Stabile $stabile): void
    {
        $tipoEdificio = $stabile->tipo_edificio;
        $numeroPiani = $stabile->numero_piani ?? 3;
        
        if ($tipoEdificio === 'condominio') {
            $this->createUnitaCondominio($stabile, $numeroPiani);
        } elseif ($tipoEdificio === 'villetta') {
            $this->createUnitaVilletta($stabile);
        } elseif ($tipoEdificio === 'complesso') {
            $this->createUnitaComplesso($stabile, $numeroPiani);
        }
    }
    
    private function createUnitaCondominio(Stabile $stabile, int $numeroPiani): void
    {
        $subCount = 1;
        
        // Appartamenti per piano
        for ($piano = 1; $piano <= $numeroPiani; $piano++) {
            $unitaPerPiano = $piano === 1 ? 3 : 4; // Piano terra meno unità
            
            for ($unita = 1; $unita <= $unitaPerPiano; $unita++) {
                $interno = "A{$piano}{$unita}";
                
                UnitaImmobiliare::create([
                    'stabile_id' => $stabile->id,
                    'interno' => $interno,
                    'piano' => $piano,
                    'scala' => 'A',
                    'denominazione' => "Appartamento piano {$piano}",
                    'tipo_unita' => 'appartamento',
                    'destinazione_uso' => 'abitazione',
                    'foglio' => $stabile->foglio,
                    'particella' => $stabile->particella,
                    'subalterno' => str_pad($subCount++, 3, '0', STR_PAD_LEFT),
                    'categoria_catastale' => $piano <= 2 ? 'A/2' : 'A/3',
                    'classe' => '3',
                    'consistenza' => rand(4, 6) . ' vani',
                    'superficie_commerciale' => rand(60, 120),
                    'superficie_calpestabile' => rand(55, 100),
                    'rendita_catastale' => rand(300, 800),
                    'numero_vani' => rand(3, 5),
                    'numero_bagni' => rand(1, 2),
                    'numero_balconi' => rand(1, 3),
                    'has_terrazzo' => $piano === $numeroPiani && rand(0, 1),
                    'has_cantina' => rand(0, 1),
                    'millesimi_generali' => rand(80, 120),
                    'millesimi_riscaldamento' => $stabile->riscaldamento_centralizzato ? rand(80, 120) : 0,
                    'millesimi_ascensore' => $stabile->ascensore && $piano > 1 ? rand(80, 120) : 0,
                    'stato_occupazione' => ['occupata_proprietario', 'occupata_inquilino', 'libera'][rand(0, 2)],
                    'is_active' => true,
                ]);
            }
        }
        
        // Box/garage al piano seminterrato
        if (rand(0, 1)) {
            for ($box = 1; $box <= 8; $box++) {
                UnitaImmobiliare::create([
                    'stabile_id' => $stabile->id,
                    'interno' => "Box{$box}",
                    'piano' => -1,
                    'denominazione' => "Box auto n. {$box}",
                    'tipo_unita' => 'box',
                    'destinazione_uso' => 'deposito',
                    'foglio' => $stabile->foglio,
                    'particella' => $stabile->particella,
                    'subalterno' => str_pad($subCount++, 3, '0', STR_PAD_LEFT),
                    'categoria_catastale' => 'C/6',
                    'superficie_commerciale' => rand(15, 25),
                    'superficie_calpestabile' => rand(15, 25),
                    'rendita_catastale' => rand(50, 150),
                    'millesimi_generali' => rand(5, 15),
                    'stato_occupazione' => ['occupata_proprietario', 'libera'][rand(0, 1)],
                    'is_active' => true,
                ]);
            }
        }
    }
    
    private function createUnitaVilletta(Stabile $stabile): void
    {
        // Villetta = un'unica unità
        UnitaImmobiliare::create([
            'stabile_id' => $stabile->id,
            'interno' => 'VILLA',
            'piano' => 0,
            'denominazione' => 'Villa unifamiliare',
            'tipo_unita' => 'appartamento',
            'destinazione_uso' => 'abitazione',
            'foglio' => $stabile->foglio,
            'particella' => $stabile->particella,
            'subalterno' => '001',
            'categoria_catastale' => 'A/7',
            'classe' => '4',
            'consistenza' => '8 vani',
            'superficie_commerciale' => rand(150, 250),
            'superficie_calpestabile' => rand(130, 220),
            'rendita_catastale' => rand(800, 1500),
            'numero_vani' => rand(6, 8),
            'numero_bagni' => rand(2, 3),
            'numero_balconi' => rand(2, 4),
            'has_terrazzo' => true,
            'has_giardino' => true,
            'has_cantina' => true,
            'millesimi_generali' => 1000, // Unica unità = 1000/1000
            'stato_occupazione' => 'occupata_proprietario',
            'is_active' => true,
        ]);
    }
    
    private function createUnitaComplesso(Stabile $stabile, int $numeroPiani): void
    {
        $subCount = 1;
        $scale = ['A', 'B', 'C'];
        
        foreach ($scale as $scala) {
            for ($piano = 1; $piano <= $numeroPiani; $piano++) {
                for ($unita = 1; $unita <= 2; $unita++) {
                    $interno = "{$scala}{$piano}{$unita}";
                    
                    UnitaImmobiliare::create([
                        'stabile_id' => $stabile->id,
                        'interno' => $interno,
                        'piano' => $piano,
                        'scala' => $scala,
                        'denominazione' => "Appartamento scala {$scala} piano {$piano}",
                        'tipo_unita' => 'appartamento',
                        'destinazione_uso' => 'abitazione',
                        'foglio' => $stabile->foglio,
                        'particella' => $stabile->particella,
                        'subalterno' => str_pad($subCount++, 3, '0', STR_PAD_LEFT),
                        'categoria_catastale' => 'A/2',
                        'classe' => '3',
                        'superficie_commerciale' => rand(70, 130),
                        'superficie_calpestabile' => rand(65, 115),
                        'rendita_catastale' => rand(400, 900),
                        'numero_vani' => rand(3, 5),
                        'numero_bagni' => rand(1, 2),
                        'numero_balconi' => rand(1, 2),
                        'has_terrazzo' => $piano === $numeroPiani && rand(0, 1),
                        'millesimi_generali' => rand(30, 50),
                        'millesimi_riscaldamento' => $stabile->riscaldamento_centralizzato ? rand(30, 50) : 0,
                        'millesimi_ascensore' => $stabile->ascensore && $piano > 1 ? rand(30, 50) : 0,
                        'millesimi_scala' => rand(15, 25),
                        'stato_occupazione' => ['occupata_proprietario', 'occupata_inquilino', 'libera'][rand(0, 2)],
                        'is_active' => true,
                    ]);
                }
            }
        }
        
        // Locali commerciali al piano terra
        for ($locale = 1; $locale <= 3; $locale++) {
            UnitaImmobiliare::create([
                'stabile_id' => $stabile->id,
                'interno' => "LC{$locale}",
                'piano' => 0,
                'denominazione' => "Locale commerciale n. {$locale}",
                'tipo_unita' => 'locale_commerciale',
                'destinazione_uso' => 'commerciale',
                'foglio' => $stabile->foglio,
                'particella' => $stabile->particella,
                'subalterno' => str_pad($subCount++, 3, '0', STR_PAD_LEFT),
                'categoria_catastale' => 'C/1',
                'superficie_commerciale' => rand(40, 80),
                'superficie_calpestabile' => rand(35, 70),
                'rendita_catastale' => rand(200, 500),
                'numero_bagni' => 1,
                'millesimi_generali' => rand(20, 40),
                'stato_occupazione' => ['occupata_inquilino', 'sfitto'][rand(0, 1)],
                'is_active' => true,
            ]);
        }
    }
}

🎯 UTILIZZO PATTERN

Query Base:

// Unità di uno stabile
$unita = UnitaImmobiliare::where('stabile_id', $stabile->id)
    ->orderBy('piano')
    ->orderBy('interno')
    ->get();

// Appartamenti occupati
$occupati = UnitaImmobiliare::occupate()
    ->byTipo('appartamento')
    ->with(['proprietari', 'inquilini'])
    ->get();

// Unità libere per affitto
$libere = UnitaImmobiliare::libere()
    ->with('stabile')
    ->get();

🎯 TABELLA UNITÀ IMMOBILIARI COMPLETATA
Con dati catastali, millesimi e collegamenti per soggetti

📖 Prossimo: soggetti.md per proprietari e inquilini collegati alle unità