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

727 lines
23 KiB
Markdown

# 🏢 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**
```sql
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
<?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
<?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
<?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**:
```php
// 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**:
```php
// 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
<?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**:
```php
// 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**:
```blade
{{-- 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**