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

{{ $unita->denominazione }}

{{ $unita->stabile->denominazione }} - {{ $unita->piano }}° piano
Millesimi Proprietà

{{ number_format($unita->millesimi_proprieta, 4) }}‰

Superficie Comm.

{{ number_format($unita->superficie_commerciale, 2) }} m²

Proprietario Attuale
{{ $unita->proprietarioAttuale?->denominazione ?? 'Non assegnato' }}
Stato
{{ $unita->stato_conservazione }}
@include('admin.unita.tabs.generale')
@include('admin.unita.tabs.millesimi')
@include('admin.unita.tabs.subentri')
@include('admin.unita.tabs.composizione')
@include('admin.unita.tabs.analytics')
@endsection ``` ## 🔧 **MODELS ELOQUENT** ### **UnitaImmobiliare.php (Estensione)** ```php '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 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