netgescon-master/docs/specifiche/MODULO_UNITA_IMMOBILIARI_AVANZATO.md
2025-07-20 14:57:25 +00:00

23 KiB

🏠 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)

-- 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

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

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

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

// 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

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

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