# 📅 TABELLA GESTIONI ## Periodi amministrativi annuali per ogni stabile --- ## 🎯 **CARATTERISTICHE TABELLA** ### **Scopo**: Tabella per gestire i periodi amministrativi di ogni stabile - tipicamente annuali (1 Gen - 31 Dic) ### **Principio**: **Collegamento via stabile** - ogni gestione appartiene a uno stabile → isolamento automatico per amministratore --- ## 🗄️ **STRUTTURA TABELLA** ```sql CREATE TABLE gestioni ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, stabile_id BIGINT UNSIGNED NOT NULL, -- FK verso stabili -- Periodo gestionale anno_gestione YEAR NOT NULL, -- Anno di riferimento (2024, 2025, ecc) data_inizio DATE NOT NULL, -- Tipicamente 01/01/YYYY data_fine DATE NOT NULL, -- Tipicamente 31/12/YYYY denominazione VARCHAR(255) NULL, -- "Gestione 2024" o custom -- Stato gestione stato ENUM('pianificata', 'attiva', 'chiusa', 'archiviata') DEFAULT 'pianificata', is_active BOOLEAN DEFAULT TRUE, data_approvazione_bilancio DATE NULL, -- Quando l'assemblea approva data_chiusura DATE NULL, -- Quando si chiude contabilmente -- Budget e limiti budget_preventivo DECIMAL(12, 2) DEFAULT 0.00, -- Budget previsto budget_consuntivo DECIMAL(12, 2) DEFAULT 0.00, -- Budget effettivo limite_spese_straordinarie DECIMAL(10, 2) NULL, -- Limite per spese senza assemblea -- Configurazioni gestione usa_fondo_di_riserva BOOLEAN DEFAULT TRUE, percentuale_fondo_riserva DECIMAL(5, 2) DEFAULT 10.00, -- % da accantonare rata_ordinaria_mensile DECIMAL(8, 2) DEFAULT 0.00, -- Rata mensile media -- Dati assembleare data_ultima_assemblea DATE NULL, data_prossima_assemblea DATE NULL, verbale_assemblea_path VARCHAR(500) NULL, -- Path al verbale -- Millesimi e ripartizioni usa_millesimi_generali BOOLEAN DEFAULT TRUE, usa_millesimi_riscaldamento BOOLEAN DEFAULT FALSE, usa_millesimi_ascensore BOOLEAN DEFAULT FALSE, configurazione_millesimi JSON NULL, -- Config personalizzate -- Note e configurazioni note_gestione TEXT NULL, configurazioni JSON NULL, -- Config specifiche gestione -- 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_anno_gestione (anno_gestione), INDEX idx_stato (stato), INDEX idx_active (is_active), INDEX idx_data_inizio (data_inizio), INDEX idx_data_fine (data_fine), -- Indici composti INDEX idx_stabile_anno (stabile_id, anno_gestione), INDEX idx_stabile_active (stabile_id, is_active), -- Vincolo unicità: un solo anno per stabile UNIQUE KEY unique_stabile_anno (stabile_id, anno_gestione) ); ``` --- ## 🏗️ **MIGRATION LARAVEL** ### **File**: `database/migrations/2024_08_05_000020_create_gestioni_table.php` ```php id(); $table->foreignId('stabile_id') ->constrained('stabili') ->onDelete('cascade'); // Periodo gestionale $table->year('anno_gestione'); $table->date('data_inizio'); $table->date('data_fine'); $table->string('denominazione')->nullable(); // Stato gestione $table->enum('stato', ['pianificata', 'attiva', 'chiusa', 'archiviata']) ->default('pianificata'); $table->boolean('is_active')->default(true); $table->date('data_approvazione_bilancio')->nullable(); $table->date('data_chiusura')->nullable(); // Budget e limiti $table->decimal('budget_preventivo', 12, 2)->default(0.00); $table->decimal('budget_consuntivo', 12, 2)->default(0.00); $table->decimal('limite_spese_straordinarie', 10, 2)->nullable(); // Configurazioni gestione $table->boolean('usa_fondo_di_riserva')->default(true); $table->decimal('percentuale_fondo_riserva', 5, 2)->default(10.00); $table->decimal('rata_ordinaria_mensile', 8, 2)->default(0.00); // Dati assembleare $table->date('data_ultima_assemblea')->nullable(); $table->date('data_prossima_assemblea')->nullable(); $table->string('verbale_assemblea_path', 500)->nullable(); // Millesimi e ripartizioni $table->boolean('usa_millesimi_generali')->default(true); $table->boolean('usa_millesimi_riscaldamento')->default(false); $table->boolean('usa_millesimi_ascensore')->default(false); $table->json('configurazione_millesimi')->nullable(); // Note e configurazioni $table->text('note_gestione')->nullable(); $table->json('configurazioni')->nullable(); $table->timestamps(); // Indici $table->index(['stabile_id']); $table->index(['anno_gestione']); $table->index(['stato']); $table->index(['is_active']); $table->index(['data_inizio']); $table->index(['data_fine']); // Indici composti $table->index(['stabile_id', 'anno_gestione'], 'idx_stabile_anno'); $table->index(['stabile_id', 'is_active'], 'idx_stabile_active'); // Vincolo unicità $table->unique(['stabile_id', 'anno_gestione'], 'unique_stabile_anno'); }); } public function down(): void { Schema::dropIfExists('gestioni'); } }; ``` --- ## 📊 **MODEL ELOQUENT** ### **File**: `app/Models/Gestione.php` ```php 'date', 'data_fine' => 'date', 'data_approvazione_bilancio' => 'date', 'data_chiusura' => 'date', 'data_ultima_assemblea' => 'date', 'data_prossima_assemblea' => 'date', 'is_active' => 'boolean', 'usa_fondo_di_riserva' => 'boolean', 'usa_millesimi_generali' => 'boolean', 'usa_millesimi_riscaldamento' => 'boolean', 'usa_millesimi_ascensore' => 'boolean', 'budget_preventivo' => 'decimal:2', 'budget_consuntivo' => 'decimal:2', 'limite_spese_straordinarie' => 'decimal:2', 'percentuale_fondo_riserva' => 'decimal:2', 'rata_ordinaria_mensile' => 'decimal:2', 'anno_gestione' => 'integer', 'configurazione_millesimi' => 'array', 'configurazioni' => 'array', ]; // Relazioni public function stabile(): BelongsTo { return $this->belongsTo(Stabile::class); } public function contabilitaRegistrazioni(): HasMany { return $this->hasMany(ContabilitaRegistrazione::class); } public function movimentiContabili(): HasMany { return $this->hasMany(MovimentoContabile::class); } public function bilanci(): HasMany { return $this->hasMany(Bilancio::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 scopeByStato(Builder $query, string $stato): Builder { return $query->where('stato', $stato); } public function scopeByAnno(Builder $query, int $anno): Builder { return $query->where('anno_gestione', $anno); } public function scopeInCorso(Builder $query): Builder { $oggi = now()->toDateString(); return $query->where('data_inizio', '<=', $oggi) ->where('data_fine', '>=', $oggi); } public function scopeScadute(Builder $query): Builder { return $query->where('data_fine', '<', now()->toDateString()) ->where('stato', '!=', 'chiusa'); } public function scopeConAssembleaProssima(Builder $query, int $giorni = 30): Builder { return $query->whereNotNull('data_prossima_assemblea') ->whereBetween('data_prossima_assemblea', [ now()->toDateString(), now()->addDays($giorni)->toDateString() ]); } // Accessors public function getDenominazioneComputedAttribute(): string { return $this->denominazione ?: "Gestione {$this->anno_gestione}"; } public function getIsInCorsoAttribute(): bool { $oggi = now()->toDate(); return $this->data_inizio <= $oggi && $this->data_fine >= $oggi; } public function getIsScadutaAttribute(): bool { return $this->data_fine < now()->toDate() && $this->stato !== 'chiusa'; } public function getGiorniRimanentiAttribute(): int { if ($this->is_scaduta) { return 0; } return now()->diffInDays($this->data_fine, false); } public function getPercentualeCompletatoAttribute(): float { $totaleGiorni = $this->data_inizio->diffInDays($this->data_fine); $giorniTrascorsi = $this->data_inizio->diffInDays(now()); if ($totaleGiorni <= 0) return 100.0; $percentuale = ($giorniTrascorsi / $totaleGiorni) * 100; return min(100.0, max(0.0, $percentuale)); } public function getBudgetUtilizzatoAttribute(): float { if ($this->budget_preventivo <= 0) return 0.0; return ($this->budget_consuntivo / $this->budget_preventivo) * 100; } public function getStatoColorAttribute(): string { return match($this->stato) { 'pianificata' => 'blue', 'attiva' => 'green', 'chiusa' => 'gray', 'archiviata' => 'purple', default => 'gray' }; } // Helper Methods public function calcolaBudgetConsuntivo(): void { $totaleSpese = $this->contabilitaRegistrazioni() ->where('tipo_movimento', 'uscita') ->sum('importo'); $this->update(['budget_consuntivo' => $totaleSpese]); } public function calcolaRataOrdinariaMensile(): void { $unitaCount = $this->stabile->numero_unita; if ($unitaCount > 0 && $this->budget_preventivo > 0) { $rataMensile = $this->budget_preventivo / 12 / $unitaCount; $this->update(['rata_ordinaria_mensile' => $rataMensile]); } } public function iniziaGestione(): bool { if ($this->stato !== 'pianificata') { return false; } // Chiudi eventuali gestioni attive per lo stesso stabile $this->stabile->gestioni() ->where('id', '!=', $this->id) ->where('stato', 'attiva') ->update(['stato' => 'chiusa', 'is_active' => false]); $this->update([ 'stato' => 'attiva', 'is_active' => true, 'data_inizio' => $this->data_inizio ?: now()->startOfYear(), ]); return true; } public function chiudiGestione(): bool { if (!in_array($this->stato, ['attiva', 'pianificata'])) { return false; } $this->calcolaBudgetConsuntivo(); $this->update([ 'stato' => 'chiusa', 'data_chiusura' => now()->toDateString(), 'is_active' => false, ]); return true; } public function archiviaGestione(): bool { if ($this->stato !== 'chiusa') { return false; } $this->update(['stato' => 'archiviata']); return true; } public function generaBilancio(): array { $entrate = $this->contabilitaRegistrazioni() ->where('tipo_movimento', 'entrata') ->sum('importo'); $uscite = $this->contabilitaRegistrazioni() ->where('tipo_movimento', 'uscita') ->sum('importo'); $saldo = $entrate - $uscite; $fondoRiserva = $this->usa_fondo_di_riserva ? ($entrate * $this->percentuale_fondo_riserva / 100) : 0; return [ 'entrate_totali' => $entrate, 'uscite_totali' => $uscite, 'saldo' => $saldo, 'fondo_riserva' => $fondoRiserva, 'saldo_disponibile' => $saldo - $fondoRiserva, 'budget_preventivo' => $this->budget_preventivo, 'scostamento_budget' => $uscite - $this->budget_preventivo, 'percentuale_utilizzo_budget' => $this->budget_utilizzato, ]; } public function canDelete(): bool { return $this->contabilitaRegistrazioni()->count() === 0; } // Observer Events protected static function boot(): void { parent::boot(); static::creating(function ($gestione) { // Genera denominazione automatica se non specificata if (!$gestione->denominazione) { $gestione->denominazione = "Gestione {$gestione->anno_gestione}"; } // Imposta date default se non specificate if (!$gestione->data_inizio) { $gestione->data_inizio = Carbon::createFromDate($gestione->anno_gestione, 1, 1); } if (!$gestione->data_fine) { $gestione->data_fine = Carbon::createFromDate($gestione->anno_gestione, 12, 31); } }); static::saved(function ($gestione) { // Ricalcola rata mensile quando cambia il budget if ($gestione->wasChanged('budget_preventivo')) { $gestione->calcolaRataOrdinariaMensile(); } }); } } ``` --- ## 🌱 **SEEDER DATI DEMO** ### **File**: `database/seeders/GestioniSeeder.php` ```php $stabile->id, 'anno_gestione' => 2023, 'data_inizio' => '2023-01-01', 'data_fine' => '2023-12-31', 'denominazione' => 'Gestione 2023', 'stato' => 'chiusa', 'is_active' => false, 'data_approvazione_bilancio' => '2024-03-15', 'data_chiusura' => '2024-01-31', 'budget_preventivo' => 50000.00, 'budget_consuntivo' => 48750.50, 'limite_spese_straordinarie' => 5000.00, 'usa_fondo_di_riserva' => true, 'percentuale_fondo_riserva' => 10.00, 'rata_ordinaria_mensile' => 200.00, 'data_ultima_assemblea' => '2024-03-15', 'usa_millesimi_generali' => true, 'usa_millesimi_riscaldamento' => $stabile->riscaldamento_centralizzato, 'usa_millesimi_ascensore' => $stabile->ascensore, 'note_gestione' => 'Gestione conclusa regolarmente. Avanzo di cassa €1.249,50', ]); // Gestione 2024 - Attiva Gestione::create([ 'stabile_id' => $stabile->id, 'anno_gestione' => 2024, 'data_inizio' => '2024-01-01', 'data_fine' => '2024-12-31', 'denominazione' => 'Gestione 2024', 'stato' => 'attiva', 'is_active' => true, 'data_approvazione_bilancio' => '2024-03-15', 'budget_preventivo' => 55000.00, 'budget_consuntivo' => 32450.75, // Parziale (agosto) 'limite_spese_straordinarie' => 6000.00, 'usa_fondo_di_riserva' => true, 'percentuale_fondo_riserva' => 12.00, 'rata_ordinaria_mensile' => 220.00, 'data_ultima_assemblea' => '2024-03-15', 'data_prossima_assemblea' => '2025-03-15', 'usa_millesimi_generali' => true, 'usa_millesimi_riscaldamento' => $stabile->riscaldamento_centralizzato, 'usa_millesimi_ascensore' => $stabile->ascensore, 'configurazione_millesimi' => [ 'millesimi_generali' => true, 'millesimi_riscaldamento' => $stabile->riscaldamento_centralizzato, 'millesimi_ascensore' => $stabile->ascensore, 'calcolo_automatico' => true, ], 'note_gestione' => 'Gestione in corso. Previsione chiusura in pareggio.', ]); // Gestione 2025 - Pianificata (solo per stabili professional/enterprise) if ($stabile->amministratore->piano_abbonamento !== 'base') { Gestione::create([ 'stabile_id' => $stabile->id, 'anno_gestione' => 2025, 'data_inizio' => '2025-01-01', 'data_fine' => '2025-12-31', 'denominazione' => 'Gestione 2025', 'stato' => 'pianificata', 'is_active' => false, 'budget_preventivo' => 58000.00, 'limite_spese_straordinarie' => 7000.00, 'usa_fondo_di_riserva' => true, 'percentuale_fondo_riserva' => 15.00, 'rata_ordinaria_mensile' => 240.00, 'data_prossima_assemblea' => '2025-03-15', 'usa_millesimi_generali' => true, 'usa_millesimi_riscaldamento' => $stabile->riscaldamento_centralizzato, 'usa_millesimi_ascensore' => $stabile->ascensore, 'note_gestione' => 'Gestione in fase di pianificazione. Budget incrementato del 5%.', ]); } } $totalGestioni = Gestione::count(); $this->command->info("✅ Create {$totalGestioni} gestioni per tutti gli stabili"); // Mostra riassunto per amministratore $stabili->groupBy('amministratore_id')->each(function ($stabiliGroup) { $admin = $stabiliGroup->first()->amministratore; $gestioniCount = Gestione::whereIn('stabile_id', $stabiliGroup->pluck('id'))->count(); $this->command->info("Admin {$admin->codice_univoco}: {$gestioniCount} gestioni"); }); } } ``` --- ## 🔍 **QUERY PATTERNS** ### **Dashboard Gestioni**: ```php // Controller - Overview gestioni per amministratore public function dashboard() { $amministratore = auth()->user()->amministratore; $stats = [ 'gestioni_attive' => Gestione::byStato('attiva')->count(), 'gestioni_in_corso' => Gestione::inCorso()->count(), 'gestioni_scadute' => Gestione::scadute()->count(), 'assemblee_prossime' => Gestione::conAssembleaProssima()->count(), // Budget aggregato 'budget_totale_preventivo' => Gestione::byStato('attiva')->sum('budget_preventivo'), 'budget_totale_consuntivo' => Gestione::byStato('attiva')->sum('budget_consuntivo'), // Rate mensili 'rate_mensili_totali' => Gestione::byStato('attiva')->sum('rata_ordinaria_mensile'), ]; // Gestioni con scadenze $gestioniScadenze = Gestione::with('stabile') ->where(function($query) { $query->conAssembleaProssima(30) ->orWhere('scadute'); }) ->orderBy('data_prossima_assemblea') ->limit(10) ->get(); return view('gestioni.dashboard', compact('stats', 'gestioniScadenze')); } ``` ### **Lista Gestioni per Stabile**: ```php // Controller - Gestioni di uno stabile specifico public function indexByStabile(Stabile $stabile) { $gestioni = $stabile->gestioni() ->orderByDesc('anno_gestione') ->with(['contabilitaRegistrazioni']) ->get() ->map(function($gestione) { $bilancio = $gestione->generaBilancio(); $gestione->bilancio_computed = $bilancio; return $gestione; }); return view('gestioni.index', compact('stabile', 'gestioni')); } ``` ### **Ricerca Avanzata**: ```php // Controller - Ricerca gestioni con filtri public function search(Request $request) { $query = Gestione::with(['stabile']); if ($request->filled('anno')) { $query->byAnno($request->anno); } if ($request->filled('stato')) { $query->byStato($request->stato); } if ($request->filled('stabile_id')) { $query->where('stabile_id', $request->stabile_id); } if ($request->filled('scadenze')) { if ($request->scadenze === 'assemblee_prossime') { $query->conAssembleaProssima(); } elseif ($request->scadenze === 'gestioni_scadute') { $query->scadute(); } } $gestioni = $query->orderByDesc('anno_gestione') ->orderBy('denominazione') ->paginate(20); return view('gestioni.search', compact('gestioni')); } ``` --- ## 🎛️ **FORM VALIDATION** ### **File**: `app/Http/Requests/GestioneRequest.php` ```php check(); } public function rules(): array { $gestioneId = $this->route('gestione')?->id; $stabileId = $this->input('stabile_id'); return [ 'stabile_id' => 'required|exists:stabili,id', 'anno_gestione' => [ 'required', 'integer', 'min:2020', 'max:' . (date('Y') + 5), Rule::unique('gestioni') ->where('stabile_id', $stabileId) ->ignore($gestioneId) ], 'data_inizio' => 'required|date', 'data_fine' => 'required|date|after:data_inizio', 'denominazione' => 'nullable|string|max:255', 'stato' => 'required|in:pianificata,attiva,chiusa,archiviata', 'budget_preventivo' => 'required|numeric|min:0|max:999999999.99', 'limite_spese_straordinarie' => 'nullable|numeric|min:0', 'percentuale_fondo_riserva' => 'required|numeric|min:0|max:50', 'rata_ordinaria_mensile' => 'nullable|numeric|min:0', 'data_approvazione_bilancio' => 'nullable|date', 'data_ultima_assemblea' => 'nullable|date', 'data_prossima_assemblea' => 'nullable|date|after:today', 'usa_fondo_di_riserva' => 'boolean', 'usa_millesimi_generali' => 'boolean', 'usa_millesimi_riscaldamento' => 'boolean', 'usa_millesimi_ascensore' => 'boolean', ]; } public function messages(): array { return [ 'stabile_id.required' => 'Devi selezionare uno stabile.', 'anno_gestione.required' => 'L\'anno di gestione è obbligatorio.', 'anno_gestione.unique' => 'Esiste già una gestione per questo anno e stabile.', 'data_inizio.required' => 'La data di inizio è obbligatoria.', 'data_fine.required' => 'La data di fine è obbligatoria.', 'data_fine.after' => 'La data di fine deve essere successiva alla data di inizio.', 'budget_preventivo.required' => 'Il budget preventivo è obbligatorio.', 'percentuale_fondo_riserva.max' => 'La percentuale del fondo di riserva non può superare il 50%.', 'data_prossima_assemblea.after' => 'La data della prossima assemblea deve essere futura.', ]; } } ``` --- ## 📋 **FORM BLADE** ### **File**: `resources/views/gestioni/form.blade.php` ```blade
``` --- ## 🎯 **UTILIZZO PATTERN** ### **Nel Controller**: ```php // Gestioni dello stabile corrente $gestioni = Gestione::where('stabile_id', $stabile->id) ->orderByDesc('anno_gestione') ->get(); // Gestione attiva corrente $gestioneAttiva = Gestione::byStato('attiva') ->where('stabile_id', $stabile->id) ->first(); // Assemblee in scadenza $assembleeProssime = Gestione::conAssembleaProssima(15)->get(); ``` ### **Nei Blade Templates**: ```blade {{-- Card gestione --}} @foreach($gestioni as $gestione){{ $gestione->stabile->denominazione }}
{{ $gestione->data_inizio->format('d/m/Y') }} - {{ $gestione->data_fine->format('d/m/Y') }}
Budget: €{{ number_format($gestione->budget_preventivo, 2) }}
@if($gestione->is_in_corso){{ round($gestione->percentuale_completato) }}% completato