# 🏠 TABELLA UNITA IMMOBILIARI ## Appartamenti, locali e box collegati agli stabili --- ## 🔑 **CONNESSIONE DATABASE** ### **Credenziali NetGescon**: ```bash # 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** ```sql 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 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 '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 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**: ```php // 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à**