1004 lines
36 KiB
Markdown
1004 lines
36 KiB
Markdown
# 📅 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
|
|
<?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
|
|
<?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
|
|
<?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**:
|
|
```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
|
|
<?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`
|
|
|
|
```blade
|
|
<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**:
|
|
```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)
|
|
<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**
|