netgescon-master/docs/02-architettura-laravel/specifiche/MODULO_UNITA_IMMOBILIARI_AVANZATO.md
Pikappa2 480e7eafbd 🎯 NETGESCON - Setup iniziale repository completo
📋 Commit iniziale con:
-  Documentazione unificata in docs/
-  Codice Laravel in netgescon-laravel/
-  Script automazione in scripts/
-  Configurazione sync rsync
-  Struttura organizzata e pulita

🔄 Versione: 2025.07.19-1644
🎯 Sistema pronto per Git distribuito
2025-07-19 16:44:47 +02:00

675 lines
23 KiB
Markdown

# 🏠 MODULO UNITÀ IMMOBILIARI AVANZATO
> **📋 Specifiche tecniche complete**
> Aggiornato: 15 Luglio 2025
> Base per: Sprint 3 Implementazione
## 🎯 **OBIETTIVI MODULO**
### **FUNZIONALITÀ INNOVATIVE**
-**Millesimi multipli** (proprietà, riscaldamento, ascensore, scale, etc.)
- 🔄 **Gestione subentri** automatici con storico
- 📊 **Calcolo ripartizioni** automatico per tipo spesa
- 🏗️ **Composizione unità** (unione/divisione automatica)
- 🔑 **Collegamento chiavi** e controlli accessi
- 📈 **Analytics occupazione** e andamento millesimi
## 🏗️ **STRUTTURA DATABASE**
### **Tabella unita_immobiliari (ESTENSIONE)**
```sql
-- Campi aggiuntivi per modulo avanzato
ALTER TABLE unita_immobiliari ADD COLUMN (
-- Millesimi dettagliati
millesimi_proprieta DECIMAL(8,4) DEFAULT 0,
millesimi_riscaldamento DECIMAL(8,4) DEFAULT 0,
millesimi_ascensore DECIMAL(8,4) DEFAULT 0,
millesimi_scale DECIMAL(8,4) DEFAULT 0,
millesimi_pulizie DECIMAL(8,4) DEFAULT 0,
millesimi_custom_1 DECIMAL(8,4) DEFAULT 0,
millesimi_custom_2 DECIMAL(8,4) DEFAULT 0,
millesimi_custom_3 DECIMAL(8,4) DEFAULT 0,
-- Dati tecnici avanzati
superficie_commerciale DECIMAL(8,2),
superficie_calpestabile DECIMAL(8,2),
superficie_balconi DECIMAL(8,2),
superficie_terrazzi DECIMAL(8,2),
numero_vani TINYINT,
numero_bagni TINYINT,
numero_balconi TINYINT,
classe_energetica VARCHAR(5),
anno_costruzione YEAR,
anno_ristrutturazione YEAR,
-- Stato e condizione
stato_conservazione ENUM('ottimo','buono','discreto','cattivo'),
necessita_lavori BOOLEAN DEFAULT FALSE,
note_tecniche TEXT,
-- Collegamento struttura fisica
struttura_fisica_id BIGINT UNSIGNED,
-- Automazioni
calcolo_automatico_millesimi BOOLEAN DEFAULT TRUE,
notifiche_subentri BOOLEAN DEFAULT TRUE,
-- Metadati avanzati
created_by BIGINT UNSIGNED,
updated_by BIGINT UNSIGNED,
FOREIGN KEY (struttura_fisica_id) REFERENCES struttura_fisica_dettaglio(id),
FOREIGN KEY (created_by) REFERENCES users(id),
FOREIGN KEY (updated_by) REFERENCES users(id)
);
```
### **Tabella subentri_unita**
```sql
CREATE TABLE subentri_unita (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
unita_immobiliare_id BIGINT UNSIGNED NOT NULL,
soggetto_precedente_id BIGINT UNSIGNED,
soggetto_nuovo_id BIGINT UNSIGNED NOT NULL,
-- Dati subentro
data_subentro DATE NOT NULL,
tipo_subentro ENUM('vendita','eredita','donazione','locazione','comodato') NOT NULL,
quota_precedente DECIMAL(5,4) DEFAULT 1.0000,
quota_nuova DECIMAL(5,4) DEFAULT 1.0000,
-- Documenti
numero_atto VARCHAR(100),
data_atto DATE,
notaio VARCHAR(200),
prezzo_vendita DECIMAL(12,2),
-- Stati
stato_subentro ENUM('proposto','in_corso','completato','annullato') DEFAULT 'proposto',
data_completamento TIMESTAMP NULL,
-- Automazioni
ripartizioni_aggiornate BOOLEAN DEFAULT FALSE,
comunicazioni_inviate BOOLEAN DEFAULT FALSE,
-- Note e allegati
note TEXT,
allegati JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by BIGINT UNSIGNED,
FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
FOREIGN KEY (soggetto_precedente_id) REFERENCES soggetti(id),
FOREIGN KEY (soggetto_nuovo_id) REFERENCES soggetti(id),
FOREIGN KEY (created_by) REFERENCES users(id),
INDEX idx_subentri_unita (unita_immobiliare_id),
INDEX idx_subentri_data (data_subentro),
INDEX idx_subentri_stato (stato_subentro)
);
```
### **Tabella composizione_unita**
```sql
CREATE TABLE composizione_unita (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- Unità coinvolte
unita_originale_id BIGINT UNSIGNED, -- NULL se è una nuova composizione
unita_risultante_id BIGINT UNSIGNED NOT NULL,
-- Tipo operazione
tipo_operazione ENUM('unione','divisione','modifica') NOT NULL,
data_operazione DATE NOT NULL,
-- Dati operazione
superficie_trasferita DECIMAL(8,2),
millesimi_trasferiti DECIMAL(8,4),
vani_trasferiti TINYINT,
-- Calcoli automatici
millesimi_automatici BOOLEAN DEFAULT TRUE,
coefficiente_ripartizione DECIMAL(6,4) DEFAULT 1.0000,
-- Documenti
numero_pratica VARCHAR(100),
riferimento_catastale VARCHAR(200),
note_variazione TEXT,
-- Stati
stato_pratica ENUM('in_corso','approvata','respinta','completata') DEFAULT 'in_corso',
data_approvazione DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by BIGINT UNSIGNED,
FOREIGN KEY (unita_originale_id) REFERENCES unita_immobiliari(id),
FOREIGN KEY (unita_risultante_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id),
INDEX idx_composizione_operazione (tipo_operazione, data_operazione),
INDEX idx_composizione_stato (stato_pratica)
);
```
### **Tabella ripartizioni_spese**
```sql
CREATE TABLE ripartizioni_spese (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
stabile_id BIGINT UNSIGNED NOT NULL,
-- Configurazione ripartizione
nome_ripartizione VARCHAR(200) NOT NULL,
descrizione TEXT,
tipo_millesimi ENUM('proprieta','riscaldamento','ascensore','scale','pulizie','custom_1','custom_2','custom_3') NOT NULL,
-- Criteri calcolo
includi_pertinenze BOOLEAN DEFAULT TRUE,
includi_locazioni BOOLEAN DEFAULT TRUE,
minimo_presenza DECIMAL(5,2) DEFAULT 0.00, -- % minima presenza per essere inclusi
-- Configurazione automatica
attiva BOOLEAN DEFAULT TRUE,
aggiornamento_automatico BOOLEAN DEFAULT TRUE,
-- Validità temporale
data_inizio DATE NOT NULL,
data_fine DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by BIGINT UNSIGNED,
FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id),
UNIQUE KEY uk_ripartizioni_nome_stabile (stabile_id, nome_ripartizione),
INDEX idx_ripartizioni_tipo (tipo_millesimi),
INDEX idx_ripartizioni_attive (attiva, data_inizio, data_fine)
);
```
## 📱 **INTERFACCIA UTENTE**
### **Dashboard Unità Immobiliare**
```php
// File: resources/views/admin/unita/show.blade.php
@extends('layouts.app-universal-v2')
@section('content')
<div class="container-fluid">
<!-- Header con azioni rapide -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h4><i class="fas fa-home"></i> {{ $unita->denominazione }}</h4>
<small>{{ $unita->stabile->denominazione }} - {{ $unita->piano }}° piano</small>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<h6>Millesimi Proprietà</h6>
<h3 class="text-primary">{{ number_format($unita->millesimi_proprieta, 4) }}</h3>
</div>
<div class="col-md-3">
<h6>Superficie Comm.</h6>
<h3 class="text-info">{{ number_format($unita->superficie_commerciale, 2) }} </h3>
</div>
<div class="col-md-3">
<h6>Proprietario Attuale</h6>
<h5>{{ $unita->proprietarioAttuale?->denominazione ?? 'Non assegnato' }}</h5>
</div>
<div class="col-md-3">
<h6>Stato</h6>
<span class="badge badge-{{ $unita->getStatoBadgeColor() }}">
{{ $unita->stato_conservazione }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab Navigation -->
<ul class="nav nav-tabs" id="unitaTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#generale">
<i class="fas fa-info-circle"></i> Generale
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#millesimi">
<i class="fas fa-chart-pie"></i> Millesimi
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#subentri">
<i class="fas fa-exchange-alt"></i> Subentri
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#composizione">
<i class="fas fa-puzzle-piece"></i> Composizione
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#analytics">
<i class="fas fa-chart-line"></i> Analytics
</a>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3">
<!-- Tab Generale -->
<div class="tab-pane fade show active" id="generale">
@include('admin.unita.tabs.generale')
</div>
<!-- Tab Millesimi -->
<div class="tab-pane fade" id="millesimi">
@include('admin.unita.tabs.millesimi')
</div>
<!-- Tab Subentri -->
<div class="tab-pane fade" id="subentri">
@include('admin.unita.tabs.subentri')
</div>
<!-- Tab Composizione -->
<div class="tab-pane fade" id="composizione">
@include('admin.unita.tabs.composizione')
</div>
<!-- Tab Analytics -->
<div class="tab-pane fade" id="analytics">
@include('admin.unita.tabs.analytics')
</div>
</div>
</div>
@endsection
```
## 🔧 **MODELS ELOQUENT**
### **UnitaImmobiliare.php (Estensione)**
```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\Relations\BelongsToMany;
class UnitaImmobiliare extends Model
{
protected $table = 'unita_immobiliari';
protected $fillable = [
'stabile_id', 'denominazione', 'piano', 'interno',
'millesimi_proprieta', 'millesimi_riscaldamento', 'millesimi_ascensore',
'millesimi_scale', 'millesimi_pulizie', 'millesimi_custom_1',
'millesimi_custom_2', 'millesimi_custom_3',
'superficie_commerciale', 'superficie_calpestabile',
'superficie_balconi', 'superficie_terrazzi',
'numero_vani', 'numero_bagni', 'numero_balconi',
'classe_energetica', 'anno_costruzione', 'anno_ristrutturazione',
'stato_conservazione', 'necessita_lavori', 'note_tecniche',
'struttura_fisica_id', 'calcolo_automatico_millesimi',
'notifiche_subentri', 'created_by', 'updated_by'
];
protected $casts = [
'millesimi_proprieta' => 'decimal:4',
'millesimi_riscaldamento' => 'decimal:4',
'millesimi_ascensore' => 'decimal:4',
'millesimi_scale' => 'decimal:4',
'millesimi_pulizie' => 'decimal:4',
'millesimi_custom_1' => 'decimal:4',
'millesimi_custom_2' => 'decimal:4',
'millesimi_custom_3' => 'decimal:4',
'superficie_commerciale' => 'decimal:2',
'superficie_calpestabile' => 'decimal:2',
'superficie_balconi' => 'decimal:2',
'superficie_terrazzi' => 'decimal:2',
'necessita_lavori' => 'boolean',
'calcolo_automatico_millesimi' => 'boolean',
'notifiche_subentri' => 'boolean',
];
// === RELAZIONI ===
public function stabile(): BelongsTo
{
return $this->belongsTo(Stabile::class);
}
public function subentri(): HasMany
{
return $this->hasMany(SubentroUnita::class);
}
public function composizioni(): HasMany
{
return $this->hasMany(ComposizioneUnita::class, 'unita_risultante_id');
}
public function strutturaFisica(): BelongsTo
{
return $this->belongsTo(StrutturaFisicaDettaglio::class, 'struttura_fisica_id');
}
public function soggetti(): BelongsToMany
{
return $this->belongsToMany(Soggetto::class, 'soggetti_unita_immobiliari')
->withPivot(['quota', 'tipo_diritto', 'data_inizio', 'data_fine'])
->withTimestamps();
}
// === METODI UTILITÀ ===
public function proprietarioAttuale()
{
return $this->soggetti()
->wherePivot('tipo_diritto', 'proprietà')
->wherePivot('data_fine', null)
->first();
}
public function calcolaMillesimiAutomatici(): array
{
if (!$this->calcolo_automatico_millesimi) {
return [];
}
$totaleStabile = $this->stabile->unita()->sum('superficie_commerciale');
$coefficiente = $this->superficie_commerciale / $totaleStabile * 1000;
return [
'millesimi_proprieta' => round($coefficiente, 4),
'millesimi_riscaldamento' => $this->calcolaMillesimiRiscaldamento(),
'millesimi_ascensore' => $this->calcolaMillesimiAscensore(),
'millesimi_scale' => $this->calcolaMillesimiScale(),
'millesimi_pulizie' => round($coefficiente, 4), // Stesso di proprietà di default
];
}
private function calcolaMillesimiRiscaldamento(): float
{
// Calcolo basato su superficie + coefficienti piano/esposizione
$base = $this->superficie_commerciale;
$coefficientePiano = $this->getCoefficientePiano();
$coefficienteEsposizione = $this->getCoefficieneEsposizione();
return round($base * $coefficientePiano * $coefficienteEsposizione /
$this->stabile->getTotaleMillesimiRiscaldamento() * 1000, 4);
}
private function calcolaMillesimiAscensore(): float
{
if ($this->piano <= 0) {
return 0; // Piano terra e seminterrati non pagano ascensore
}
$coefficientePiano = max(1, $this->piano * 0.15 + 0.85); // Crescente per piano
return round($this->superficie_commerciale * $coefficientePiano /
$this->stabile->getTotaleMillesimiAscensore() * 1000, 4);
}
private function calcolaMillesimiScale(): float
{
if ($this->piano <= 0) {
return round($this->superficie_commerciale * 0.5 /
$this->stabile->getTotaleMillesimiScale() * 1000, 4);
}
return round($this->superficie_commerciale /
$this->stabile->getTotaleMillesimiScale() * 1000, 4);
}
public function getStatoBadgeColor(): string
{
return match($this->stato_conservazione) {
'ottimo' => 'success',
'buono' => 'info',
'discreto' => 'warning',
'cattivo' => 'danger',
default => 'secondary'
};
}
public function generaSubentroAutomatico(Soggetto $nuovoSoggetto, array $datiSubentro): SubentroUnita
{
$proprietarioAttuale = $this->proprietarioAttuale();
return $this->subentri()->create([
'soggetto_precedente_id' => $proprietarioAttuale?->id,
'soggetto_nuovo_id' => $nuovoSoggetto->id,
'data_subentro' => $datiSubentro['data_subentro'],
'tipo_subentro' => $datiSubentro['tipo_subentro'],
'quota_nuova' => $datiSubentro['quota'] ?? 1.0000,
'numero_atto' => $datiSubentro['numero_atto'] ?? null,
'data_atto' => $datiSubentro['data_atto'] ?? null,
'notaio' => $datiSubentro['notaio'] ?? null,
'prezzo_vendita' => $datiSubentro['prezzo_vendita'] ?? null,
'created_by' => auth()->id()
]);
}
// === SCOPES ===
public function scopeConMillesimi($query, string $tipo = 'proprieta')
{
return $query->where("millesimi_{$tipo}", '>', 0);
}
public function scopeDelPiano($query, int $piano)
{
return $query->where('piano', $piano);
}
public function scopeConSuperficieMinima($query, float $minima)
{
return $query->where('superficie_commerciale', '>=', $minima);
}
}
```
## 🚀 **CONTROLLER AVANZATO**
### **UnitaImmobiliareController.php**
```php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\UnitaImmobiliare;
use App\Models\Stabile;
use App\Models\Soggetto;
use App\Models\SubentroUnita;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class UnitaImmobiliareController extends Controller
{
public function show(UnitaImmobiliare $unita)
{
$unita->load([
'stabile',
'soggetti',
'subentri.soggettoNuovo',
'subentri.soggettoPrecedente',
'composizioni',
'strutturaFisica'
]);
$analytics = $this->calcolaAnalytics($unita);
return view('admin.unita.show', compact('unita', 'analytics'));
}
public function ricalcolaMillesimi(UnitaImmobiliare $unita)
{
if (!$unita->calcolo_automatico_millesimi) {
return response()->json([
'success' => false,
'message' => 'Calcolo automatico disabilitato per questa unità'
], 400);
}
$nuoviMillesimi = $unita->calcolaMillesimiAutomatici();
$unita->update($nuoviMillesimi);
return response()->json([
'success' => true,
'message' => 'Millesimi ricalcolati automaticamente',
'data' => $nuoviMillesimi
]);
}
public function creaSubentro(Request $request, UnitaImmobiliare $unita)
{
$request->validate([
'soggetto_nuovo_id' => 'required|exists:soggetti,id',
'data_subentro' => 'required|date',
'tipo_subentro' => 'required|in:vendita,eredita,donazione,locazione,comodato',
'quota' => 'required|numeric|min:0|max:1',
'numero_atto' => 'nullable|string|max:100',
'data_atto' => 'nullable|date',
'notaio' => 'nullable|string|max:200',
'prezzo_vendita' => 'nullable|numeric|min:0'
]);
DB::beginTransaction();
try {
$nuovoSoggetto = Soggetto::findOrFail($request->soggetto_nuovo_id);
$subentro = $unita->generaSubentroAutomatico($nuovoSoggetto, $request->all());
// Aggiorna relazione soggetti_unita_immobiliari
$this->aggiornaProprietaUnita($unita, $subentro);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Subentro creato con successo',
'subentro_id' => $subentro->id
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Errore durante la creazione del subentro: ' . $e->getMessage()
], 500);
}
}
public function approvaSubentro(SubentroUnita $subentro)
{
if ($subentro->stato_subentro !== 'proposto') {
return response()->json([
'success' => false,
'message' => 'Il subentro deve essere in stato "proposto" per essere approvato'
], 400);
}
DB::beginTransaction();
try {
$subentro->update([
'stato_subentro' => 'completato',
'data_completamento' => now(),
'ripartizioni_aggiornate' => true
]);
// Completa il passaggio di proprietà
$this->completaSubentro($subentro);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Subentro approvato e completato'
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Errore durante l\'approvazione: ' . $e->getMessage()
], 500);
}
}
private function calcolaAnalytics(UnitaImmobiliare $unita): array
{
return [
'storico_subentri' => $unita->subentri()->count(),
'composizioni_totali' => $unita->composizioni()->count(),
'superficie_totale' => $unita->superficie_commerciale +
$unita->superficie_balconi +
$unita->superficie_terrazzi,
'percentuale_millesimi' => ($unita->millesimi_proprieta / 10), // Su base 100
'valore_catastale_stimato' => $this->stimaValoreCatastale($unita),
'trend_mercato' => $this->calcolaTrendMercato($unita)
];
}
private function completaSubentro(SubentroUnita $subentro): void
{
$unita = $subentro->unitaImmobiliare;
// Chiudi relazione precedente
if ($subentro->soggetto_precedente_id) {
$unita->soggetti()
->wherePivot('id', $subentro->soggetto_precedente_id)
->updateExistingPivot($subentro->soggetto_precedente_id, [
'data_fine' => $subentro->data_subentro
]);
}
// Crea nuova relazione
$unita->soggetti()->attach($subentro->soggetto_nuovo_id, [
'quota' => $subentro->quota_nuova,
'tipo_diritto' => $this->getTipoDirittoFromSubentro($subentro->tipo_subentro),
'data_inizio' => $subentro->data_subentro,
'data_fine' => null
]);
}
private function getTipoDirittoFromSubentro(string $tipoSubentro): string
{
return match($tipoSubentro) {
'vendita', 'eredita', 'donazione' => 'proprietà',
'locazione' => 'locazione',
'comodato' => 'comodato',
default => 'proprietà'
};
}
}
```
## 🎯 **PROSSIMI PASSI IMPLEMENTAZIONE**
### **Sprint 3: Unità Immobiliari Avanzate**
1. **Database**: Creare le nuove migrazioni
2. **Models**: Estendere UnitaImmobiliare e creare i nuovi model
3. **Controller**: Implementare UnitaImmobiliareController avanzato
4. **Views**: Creare dashboard completa con tab
5. **API**: Endpoint per calcoli automatici e subentri
6. **Testing**: Test completi funzionalità avanzate
### **Milestone Raggiunta**
**Modulo Unità Immobiliari Avanzato** - Specifiche Complete
🎯 **Ready for Implementation** - Sprint 3 Fase 2