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

36 KiB

📅 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

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

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('gestioni', function (Blueprint $table) {
            $table->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

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;
use Carbon\Carbon;

class Gestione extends Model
{
    protected $fillable = [
        'stabile_id',
        'anno_gestione',
        'data_inizio',
        'data_fine',
        'denominazione',
        'stato',
        'is_active',
        'data_approvazione_bilancio',
        'data_chiusura',
        'budget_preventivo',
        'budget_consuntivo',
        'limite_spese_straordinarie',
        'usa_fondo_di_riserva',
        'percentuale_fondo_riserva',
        'rata_ordinaria_mensile',
        'data_ultima_assemblea',
        'data_prossima_assemblea',
        'verbale_assemblea_path',
        'usa_millesimi_generali',
        'usa_millesimi_riscaldamento',
        'usa_millesimi_ascensore',
        'configurazione_millesimi',
        'note_gestione',
        'configurazioni',
    ];

    protected $casts = [
        'data_inizio' => '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

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Gestione;
use App\Models\Stabile;
use Carbon\Carbon;

class GestioniSeeder extends Seeder
{
    public function run(): void
    {
        $stabili = Stabile::all();
        
        foreach ($stabili as $stabile) {
            // Gestione 2023 - Chiusa
            Gestione::create([
                'stabile_id' => $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:

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

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

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

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class GestioneRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->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

<form method="POST" action="{{ isset($gestione) ? route('gestioni.update', $gestione) : route('gestioni.store') }}">
    @csrf
    @if(isset($gestione))
        @method('PUT')
    @endif
    
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
        <!-- Stabile -->
        <div>
            <label for="stabile_id" class="block text-sm font-medium text-gray-700">Stabile</label>
            <select name="stabile_id" id="stabile_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
                <option value="">Seleziona stabile...</option>
                @foreach($stabili as $stabile)
                    <option value="{{ $stabile->id }}" 
                            {{ old('stabile_id', $gestione->stabile_id ?? '') == $stabile->id ? 'selected' : '' }}>
                        {{ $stabile->denominazione }}
                    </option>
                @endforeach
            </select>
            @error('stabile_id')
                <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
            @enderror
        </div>

        <!-- Anno Gestione -->
        <div>
            <label for="anno_gestione" class="block text-sm font-medium text-gray-700">Anno Gestione</label>
            <input type="number" 
                   name="anno_gestione" 
                   id="anno_gestione"
                   min="2020" 
                   max="{{ date('Y') + 5 }}"
                   value="{{ old('anno_gestione', $gestione->anno_gestione ?? date('Y')) }}"
                   class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
            @error('anno_gestione')
                <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
            @enderror
        </div>

        <!-- Data Inizio -->
        <div>
            <label for="data_inizio" class="block text-sm font-medium text-gray-700">Data Inizio</label>
            <input type="date" 
                   name="data_inizio" 
                   id="data_inizio"
                   value="{{ old('data_inizio', $gestione->data_inizio?->format('Y-m-d') ?? date('Y-01-01')) }}"
                   class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
            @error('data_inizio')
                <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
            @enderror
        </div>

        <!-- Data Fine -->
        <div>
            <label for="data_fine" class="block text-sm font-medium text-gray-700">Data Fine</label>
            <input type="date" 
                   name="data_fine" 
                   id="data_fine"
                   value="{{ old('data_fine', $gestione->data_fine?->format('Y-m-d') ?? date('Y-12-31')) }}"
                   class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
            @error('data_fine')
                <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
            @enderror
        </div>

        <!-- Budget Preventivo -->
        <div>
            <label for="budget_preventivo" class="block text-sm font-medium text-gray-700">Budget Preventivo (€)</label>
            <input type="number" 
                   name="budget_preventivo" 
                   id="budget_preventivo"
                   step="0.01"
                   min="0"
                   value="{{ old('budget_preventivo', $gestione->budget_preventivo ?? '') }}"
                   class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
            @error('budget_preventivo')
                <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
            @enderror
        </div>

        <!-- Percentuale Fondo Riserva -->
        <div>
            <label for="percentuale_fondo_riserva" class="block text-sm font-medium text-gray-700">Fondo Riserva (%)</label>
            <input type="number" 
                   name="percentuale_fondo_riserva" 
                   id="percentuale_fondo_riserva"
                   step="0.01"
                   min="0"
                   max="50"
                   value="{{ old('percentuale_fondo_riserva', $gestione->percentuale_fondo_riserva ?? '10.00') }}"
                   class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
            @error('percentuale_fondo_riserva')
                <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
            @enderror
        </div>

        <!-- Stato -->
        <div>
            <label for="stato" class="block text-sm font-medium text-gray-700">Stato</label>
            <select name="stato" id="stato" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
                @foreach(['pianificata', 'attiva', 'chiusa', 'archiviata'] as $stato)
                    <option value="{{ $stato }}" 
                            {{ old('stato', $gestione->stato ?? 'pianificata') === $stato ? 'selected' : '' }}>
                        {{ ucfirst($stato) }}
                    </option>
                @endforeach
            </select>
        </div>

        <!-- Checkboxes Millesimi -->
        <div class="md:col-span-2">
            <fieldset>
                <legend class="text-base font-medium text-gray-900">Configurazione Millesimi</legend>
                <div class="mt-4 space-y-4">
                    <div class="flex items-center">
                        <input id="usa_millesimi_generali" 
                               name="usa_millesimi_generali" 
                               type="checkbox" 
                               value="1"
                               {{ old('usa_millesimi_generali', $gestione->usa_millesimi_generali ?? true) ? 'checked' : '' }}
                               class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
                        <label for="usa_millesimi_generali" class="ml-3 block text-sm font-medium text-gray-700">
                            Usa millesimi generali
                        </label>
                    </div>
                    
                    <div class="flex items-center">
                        <input id="usa_millesimi_riscaldamento" 
                               name="usa_millesimi_riscaldamento" 
                               type="checkbox" 
                               value="1"
                               {{ old('usa_millesimi_riscaldamento', $gestione->usa_millesimi_riscaldamento ?? false) ? 'checked' : '' }}
                               class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
                        <label for="usa_millesimi_riscaldamento" class="ml-3 block text-sm font-medium text-gray-700">
                            Usa millesimi riscaldamento
                        </label>
                    </div>
                    
                    <div class="flex items-center">
                        <input id="usa_millesimi_ascensore" 
                               name="usa_millesimi_ascensore" 
                               type="checkbox" 
                               value="1"
                               {{ old('usa_millesimi_ascensore', $gestione->usa_millesimi_ascensore ?? false) ? 'checked' : '' }}
                               class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
                        <label for="usa_millesimi_ascensore" class="ml-3 block text-sm font-medium text-gray-700">
                            Usa millesimi ascensore
                        </label>
                    </div>
                </div>
            </fieldset>
        </div>
    </div>

    <!-- Submit Button -->
    <div class="mt-6">
        <button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
            {{ isset($gestione) ? 'Aggiorna Gestione' : 'Crea Gestione' }}
        </button>
    </div>
</form>

🎯 UTILIZZO PATTERN

Nel Controller:

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

{{-- Card gestione --}}
@foreach($gestioni as $gestione)
    <div class="card border-l-4 border-{{ $gestione->stato_color }}-500">
        <div class="flex justify-between items-center">
            <h3>{{ $gestione->denominazione_computed }}</h3>
            <span class="badge badge-{{ $gestione->stato_color }}">
                {{ ucfirst($gestione->stato) }}
            </span>
        </div>
        
        <div class="mt-2 text-sm text-gray-600">
            <p>{{ $gestione->stabile->denominazione }}</p>
            <p>{{ $gestione->data_inizio->format('d/m/Y') }} - {{ $gestione->data_fine->format('d/m/Y') }}</p>
            <p>Budget: €{{ number_format($gestione->budget_preventivo, 2) }}</p>
            
            @if($gestione->is_in_corso)
                <div class="mt-2">
                    <div class="w-full bg-gray-200 rounded-full h-2">
                        <div class="bg-blue-600 h-2 rounded-full" 
                             style="width: {{ $gestione->percentuale_completato }}%"></div>
                    </div>
                    <p class="text-xs mt-1">{{ round($gestione->percentuale_completato) }}% completato</p>
                </div>
            @endif
        </div>
    </div>
@endforeach

🎯 TABELLA GESTIONI DEFINITA
Con periodi amministrativi, budget e isolamento per amministratore tramite stabili

📖 Prossimo: unita-immobiliari.md per appartamenti e locali collegati agli stabili