netgescon-master/docs/05-backup-unificazione/DOCS-UNIFIED/02-DOCUMENTAZIONE-TECNICA/04-DATABASE-STRUTTURE.md
Pikappa2 480e7eafbd 🎯 NETGESCON - Setup iniziale repository completo
📋 Commit iniziale con:
-  Documentazione unificata in docs/
-  Codice Laravel in netgescon-laravel/
-  Script automazione in scripts/
-  Configurazione sync rsync
-  Struttura organizzata e pulita

🔄 Versione: 2025.07.19-1644
🎯 Sistema pronto per Git distribuito
2025-07-19 16:44:47 +02:00

56 KiB

4. DATABASE E STRUTTURE - GUIDA COMPLETA

📋 INDICE CAPITOLO


4.1 Schema Database Completo

Tabelle Sistema Utenti

-- Tabella utenti base
CREATE TABLE users (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    email_verified_at TIMESTAMP NULL,
    password VARCHAR(255) NOT NULL,
    remember_token VARCHAR(100) NULL,
    is_active BOOLEAN DEFAULT TRUE,
    last_login_at TIMESTAMP NULL,
    profile_photo_path VARCHAR(2048) NULL,
    phone VARCHAR(20) NULL,
    address TEXT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    INDEX idx_email (email),
    INDEX idx_active (is_active),
    INDEX idx_last_login (last_login_at)
);

-- Tabelle Spatie Permission
CREATE TABLE roles (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    guard_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    UNIQUE KEY unique_role_guard (name, guard_name)
);

CREATE TABLE permissions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    guard_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    UNIQUE KEY unique_permission_guard (name, guard_name)
);

CREATE TABLE model_has_roles (
    role_id BIGINT UNSIGNED NOT NULL,
    model_type VARCHAR(255) NOT NULL,
    model_id BIGINT UNSIGNED NOT NULL,
    PRIMARY KEY (role_id, model_id, model_type),
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
    INDEX idx_model_type (model_type),
    INDEX idx_model_id (model_id)
);

CREATE TABLE model_has_permissions (
    permission_id BIGINT UNSIGNED NOT NULL,
    model_type VARCHAR(255) NOT NULL,
    model_id BIGINT UNSIGNED NOT NULL,
    PRIMARY KEY (permission_id, model_id, model_type),
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
    INDEX idx_model_type (model_type),
    INDEX idx_model_id (model_id)
);

CREATE TABLE role_has_permissions (
    permission_id BIGINT UNSIGNED NOT NULL,
    role_id BIGINT UNSIGNED NOT NULL,
    PRIMARY KEY (permission_id, role_id),
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

Tabelle Anagrafica Base

-- Comuni italiani
CREATE TABLE comuni_italiani (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    codice_catastale VARCHAR(4) NOT NULL UNIQUE,
    denominazione VARCHAR(255) NOT NULL,
    denominazione_tedesca VARCHAR(255) NULL,
    denominazione_altra VARCHAR(255) NULL,
    codice_provincia VARCHAR(2) NOT NULL,
    provincia VARCHAR(255) NOT NULL,
    regione VARCHAR(255) NOT NULL,
    cap VARCHAR(5) NULL,
    prefisso VARCHAR(10) NULL,
    email_pec VARCHAR(255) NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    INDEX idx_codice_catastale (codice_catastale),
    INDEX idx_provincia (codice_provincia),
    INDEX idx_denominazione (denominazione),
    INDEX idx_cap (cap)
);

-- Stabili
CREATE TABLE stabili (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    denominazione VARCHAR(255) NOT NULL,
    indirizzo VARCHAR(255) NOT NULL,
    comune_id BIGINT UNSIGNED NULL,
    cap VARCHAR(10) NULL,
    codice_fiscale VARCHAR(16) NULL,
    partita_iva VARCHAR(11) NULL,
    amministratore_id BIGINT UNSIGNED NULL,
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (comune_id) REFERENCES comuni_italiani(id) ON DELETE SET NULL,
    FOREIGN KEY (amministratore_id) REFERENCES users(id) ON DELETE SET NULL,
    
    INDEX idx_denominazione (denominazione),
    INDEX idx_amministratore (amministratore_id),
    INDEX idx_comune (comune_id),
    INDEX idx_active (is_active),
    INDEX idx_codice_fiscale (codice_fiscale)
);

-- Soggetti (persone fisiche e giuridiche)
CREATE TABLE soggetti (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tipo_soggetto ENUM('persona_fisica', 'persona_giuridica') NOT NULL,
    nome VARCHAR(255) NULL,
    cognome VARCHAR(255) NULL,
    ragione_sociale VARCHAR(255) NULL,
    codice_fiscale VARCHAR(16) NULL,
    partita_iva VARCHAR(11) NULL,
    data_nascita DATE NULL,
    luogo_nascita VARCHAR(255) NULL,
    indirizzo VARCHAR(255) NULL,
    comune_id BIGINT UNSIGNED NULL,
    cap VARCHAR(10) NULL,
    telefono VARCHAR(20) NULL,
    email VARCHAR(255) NULL,
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (comune_id) REFERENCES comuni_italiani(id) ON DELETE SET NULL,
    
    INDEX idx_codice_fiscale (codice_fiscale),
    INDEX idx_partita_iva (partita_iva),
    INDEX idx_tipo_soggetto (tipo_soggetto),
    INDEX idx_cognome_nome (cognome, nome),
    INDEX idx_ragione_sociale (ragione_sociale),
    INDEX idx_active (is_active)
);

-- Unità immobiliari
CREATE TABLE unita_immobiliari (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stabile_id BIGINT UNSIGNED NOT NULL,
    numero VARCHAR(50) NOT NULL,
    piano VARCHAR(20) NULL,
    scala VARCHAR(20) NULL,
    interno VARCHAR(20) NULL,
    tipo ENUM('appartamento', 'negozio', 'garage', 'cantina', 'altro') NOT NULL DEFAULT 'appartamento',
    categoria_catastale VARCHAR(10) NULL,
    classe_catastale VARCHAR(10) NULL,
    consistenza VARCHAR(20) NULL,
    superficie_catastale DECIMAL(8,2) NULL,
    superficie_commerciale DECIMAL(8,2) NULL,
    rendita_catastale DECIMAL(10,2) NULL,
    millesimi_proprieta DECIMAL(8,4) NULL,
    millesimi_riscaldamento DECIMAL(8,4) NULL,
    millesimi_acqua DECIMAL(8,4) NULL,
    millesimi_ascensore DECIMAL(8,4) NULL,
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    
    INDEX idx_stabile (stabile_id),
    INDEX idx_numero (numero),
    INDEX idx_tipo (tipo),
    INDEX idx_active (is_active),
    UNIQUE KEY unique_stabile_numero (stabile_id, numero)
);

Tabelle Relazioni

-- Diritti reali (collega soggetti a unità immobiliari)
CREATE TABLE diritti_reali (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    soggetto_id BIGINT UNSIGNED NOT NULL,
    unita_immobiliare_id BIGINT UNSIGNED NOT NULL,
    tipo_diritto ENUM('proprietario', 'nudo_proprietario', 'usufruttuario', 'inquilino', 'comodatario') NOT NULL,
    quota_proprieta DECIMAL(8,4) NULL,
    data_inizio DATE NULL,
    data_fine DATE NULL,
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (soggetto_id) REFERENCES soggetti(id) ON DELETE CASCADE,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
    
    INDEX idx_soggetto (soggetto_id),
    INDEX idx_unita (unita_immobiliare_id),
    INDEX idx_tipo_diritto (tipo_diritto),
    INDEX idx_active (is_active),
    INDEX idx_data_inizio (data_inizio),
    INDEX idx_data_fine (data_fine)
);

-- Collegamento users a unità immobiliari (per condomini)
CREATE TABLE user_unita_immobiliari (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    unita_immobiliare_id BIGINT UNSIGNED NOT NULL,
    tipo_accesso ENUM('proprietario', 'inquilino', 'amministratore') NOT NULL,
    data_inizio DATE NULL,
    data_fine DATE NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
    
    INDEX idx_user (user_id),
    INDEX idx_unita (unita_immobiliare_id),
    INDEX idx_tipo_accesso (tipo_accesso),
    INDEX idx_active (is_active),
    UNIQUE KEY unique_user_unita (user_id, unita_immobiliare_id)
);

Tabelle Gestione Documenti

-- Documenti
CREATE TABLE documenti (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stabile_id BIGINT UNSIGNED NOT NULL,
    unita_immobiliare_id BIGINT UNSIGNED NULL,
    soggetto_id BIGINT UNSIGNED NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    titolo VARCHAR(255) NOT NULL,
    descrizione TEXT NULL,
    tipo_documento VARCHAR(100) NULL,
    categoria ENUM('generale', 'amministrativo', 'contabile', 'tecnico', 'legale') NOT NULL DEFAULT 'generale',
    nome_file VARCHAR(255) NOT NULL,
    path_file VARCHAR(500) NOT NULL,
    dimensione_file BIGINT UNSIGNED NULL,
    mime_type VARCHAR(100) NULL,
    is_public BOOLEAN DEFAULT FALSE,
    data_documento DATE NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
    FOREIGN KEY (soggetto_id) REFERENCES soggetti(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    
    INDEX idx_stabile (stabile_id),
    INDEX idx_unita (unita_immobiliare_id),
    INDEX idx_soggetto (soggetto_id),
    INDEX idx_user (user_id),
    INDEX idx_categoria (categoria),
    INDEX idx_public (is_public),
    INDEX idx_data_documento (data_documento),
    INDEX idx_created_at (created_at)
);

Tabelle Contabilità

-- Movimenti bancari
CREATE TABLE movimenti_bancari (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stabile_id BIGINT UNSIGNED NOT NULL,
    data_movimento DATE NOT NULL,
    data_valuta DATE NULL,
    descrizione TEXT NOT NULL,
    importo DECIMAL(12,2) NOT NULL,
    tipo_movimento ENUM('entrata', 'uscita') NOT NULL,
    categoria VARCHAR(100) NULL,
    sottocategoria VARCHAR(100) NULL,
    causale VARCHAR(255) NULL,
    documento_riferimento VARCHAR(255) NULL,
    conto_corrente VARCHAR(100) NULL,
    is_riconciliato BOOLEAN DEFAULT FALSE,
    user_id BIGINT UNSIGNED NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    
    INDEX idx_stabile (stabile_id),
    INDEX idx_data_movimento (data_movimento),
    INDEX idx_tipo_movimento (tipo_movimento),
    INDEX idx_categoria (categoria),
    INDEX idx_riconciliato (is_riconciliato),
    INDEX idx_importo (importo)
);

-- Ripartizioni spese
CREATE TABLE ripartizioni_spese (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    movimento_bancario_id BIGINT UNSIGNED NOT NULL,
    unita_immobiliare_id BIGINT UNSIGNED NOT NULL,
    importo_ripartito DECIMAL(12,2) NOT NULL,
    quota_millesimi DECIMAL(8,4) NULL,
    tipo_ripartizione ENUM('millesimi_proprieta', 'millesimi_riscaldamento', 'millesimi_acqua', 'millesimi_ascensore', 'fissa') NOT NULL,
    note TEXT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (movimento_bancario_id) REFERENCES movimenti_bancari(id) ON DELETE CASCADE,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
    
    INDEX idx_movimento (movimento_bancario_id),
    INDEX idx_unita (unita_immobiliare_id),
    INDEX idx_tipo_ripartizione (tipo_ripartizione)
);

Tabelle Comunicazioni

-- Tickets/Comunicazioni
CREATE TABLE tickets (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stabile_id BIGINT UNSIGNED NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    unita_immobiliare_id BIGINT UNSIGNED NULL,
    assigned_to BIGINT UNSIGNED NULL,
    titolo VARCHAR(255) NOT NULL,
    descrizione TEXT NOT NULL,
    categoria ENUM('manutenzione', 'amministrativo', 'contabile', 'segnalazione', 'richiesta', 'reclamo') NOT NULL DEFAULT 'segnalazione',
    priorita ENUM('bassa', 'media', 'alta', 'urgente') NOT NULL DEFAULT 'media',
    stato ENUM('aperto', 'in_lavorazione', 'in_attesa', 'risolto', 'chiuso') NOT NULL DEFAULT 'aperto',
    data_scadenza DATE NULL,
    data_risoluzione TIMESTAMP NULL,
    is_public BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE SET NULL,
    FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
    
    INDEX idx_stabile (stabile_id),
    INDEX idx_user (user_id),
    INDEX idx_unita (unita_immobiliare_id),
    INDEX idx_assigned (assigned_to),
    INDEX idx_categoria (categoria),
    INDEX idx_priorita (priorita),
    INDEX idx_stato (stato),
    INDEX idx_data_scadenza (data_scadenza),
    INDEX idx_created_at (created_at)
);

-- Risposte ai tickets
CREATE TABLE ticket_risposte (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ticket_id BIGINT UNSIGNED NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    messaggio TEXT NOT NULL,
    is_internal BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    
    INDEX idx_ticket (ticket_id),
    INDEX idx_user (user_id),
    INDEX idx_internal (is_internal),
    INDEX idx_created_at (created_at)
);

4.2 Migrazioni Laravel

Migration Base Users

File: database/migrations/2024_01_01_000000_create_users_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('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->boolean('is_active')->default(true);
            $table->timestamp('last_login_at')->nullable();
            $table->string('profile_photo_path', 2048)->nullable();
            $table->string('phone', 20)->nullable();
            $table->text('address')->nullable();
            $table->rememberToken();
            $table->timestamps();
            
            $table->index(['email']);
            $table->index(['is_active']);
            $table->index(['last_login_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Migration Comuni Italiani

File: database/migrations/2024_01_02_000000_create_comuni_italiani_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('comuni_italiani', function (Blueprint $table) {
            $table->id();
            $table->string('codice_catastale', 4)->unique();
            $table->string('denominazione');
            $table->string('denominazione_tedesca')->nullable();
            $table->string('denominazione_altra')->nullable();
            $table->string('codice_provincia', 2);
            $table->string('provincia');
            $table->string('regione');
            $table->string('cap', 5)->nullable();
            $table->string('prefisso', 10)->nullable();
            $table->string('email_pec')->nullable();
            $table->timestamps();
            
            $table->index(['codice_catastale']);
            $table->index(['codice_provincia']);
            $table->index(['denominazione']);
            $table->index(['cap']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('comuni_italiani');
    }
};

Migration Stabili

File: database/migrations/2024_01_03_000000_create_stabili_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('stabili', function (Blueprint $table) {
            $table->id();
            $table->string('denominazione');
            $table->string('indirizzo');
            $table->foreignId('comune_id')->nullable()->constrained('comuni_italiani')->onDelete('set null');
            $table->string('cap', 10)->nullable();
            $table->string('codice_fiscale', 16)->nullable();
            $table->string('partita_iva', 11)->nullable();
            $table->foreignId('amministratore_id')->nullable()->constrained('users')->onDelete('set null');
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            $table->timestamps();
            
            $table->index(['denominazione']);
            $table->index(['amministratore_id']);
            $table->index(['comune_id']);
            $table->index(['is_active']);
            $table->index(['codice_fiscale']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('stabili');
    }
};

Migration Soggetti

File: database/migrations/2024_01_04_000000_create_soggetti_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('soggetti', function (Blueprint $table) {
            $table->id();
            $table->enum('tipo_soggetto', ['persona_fisica', 'persona_giuridica']);
            $table->string('nome')->nullable();
            $table->string('cognome')->nullable();
            $table->string('ragione_sociale')->nullable();
            $table->string('codice_fiscale', 16)->nullable();
            $table->string('partita_iva', 11)->nullable();
            $table->date('data_nascita')->nullable();
            $table->string('luogo_nascita')->nullable();
            $table->string('indirizzo')->nullable();
            $table->foreignId('comune_id')->nullable()->constrained('comuni_italiani')->onDelete('set null');
            $table->string('cap', 10)->nullable();
            $table->string('telefono', 20)->nullable();
            $table->string('email')->nullable();
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            $table->timestamps();
            
            $table->index(['codice_fiscale']);
            $table->index(['partita_iva']);
            $table->index(['tipo_soggetto']);
            $table->index(['cognome', 'nome']);
            $table->index(['ragione_sociale']);
            $table->index(['is_active']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('soggetti');
    }
};

Migration Unità Immobiliari

File: database/migrations/2024_01_05_000000_create_unita_immobiliari_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('unita_immobiliari', function (Blueprint $table) {
            $table->id();
            $table->foreignId('stabile_id')->constrained('stabili')->onDelete('cascade');
            $table->string('numero', 50);
            $table->string('piano', 20)->nullable();
            $table->string('scala', 20)->nullable();
            $table->string('interno', 20)->nullable();
            $table->enum('tipo', ['appartamento', 'negozio', 'garage', 'cantina', 'altro'])->default('appartamento');
            $table->string('categoria_catastale', 10)->nullable();
            $table->string('classe_catastale', 10)->nullable();
            $table->string('consistenza', 20)->nullable();
            $table->decimal('superficie_catastale', 8, 2)->nullable();
            $table->decimal('superficie_commerciale', 8, 2)->nullable();
            $table->decimal('rendita_catastale', 10, 2)->nullable();
            $table->decimal('millesimi_proprieta', 8, 4)->nullable();
            $table->decimal('millesimi_riscaldamento', 8, 4)->nullable();
            $table->decimal('millesimi_acqua', 8, 4)->nullable();
            $table->decimal('millesimi_ascensore', 8, 4)->nullable();
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            $table->timestamps();
            
            $table->index(['stabile_id']);
            $table->index(['numero']);
            $table->index(['tipo']);
            $table->index(['is_active']);
            $table->unique(['stabile_id', 'numero']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('unita_immobiliari');
    }
};

4.3 Relazioni e Vincoli

Tabelle Ponte

File: database/migrations/2024_01_06_000000_create_diritti_reali_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('diritti_reali', function (Blueprint $table) {
            $table->id();
            $table->foreignId('soggetto_id')->constrained('soggetti')->onDelete('cascade');
            $table->foreignId('unita_immobiliare_id')->constrained('unita_immobiliari')->onDelete('cascade');
            $table->enum('tipo_diritto', ['proprietario', 'nudo_proprietario', 'usufruttuario', 'inquilino', 'comodatario']);
            $table->decimal('quota_proprieta', 8, 4)->nullable();
            $table->date('data_inizio')->nullable();
            $table->date('data_fine')->nullable();
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            $table->timestamps();
            
            $table->index(['soggetto_id']);
            $table->index(['unita_immobiliare_id']);
            $table->index(['tipo_diritto']);
            $table->index(['is_active']);
            $table->index(['data_inizio']);
            $table->index(['data_fine']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('diritti_reali');
    }
};

Tabella User-Unità

File: database/migrations/2024_01_07_000000_create_user_unita_immobiliari_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('user_unita_immobiliari', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
            $table->foreignId('unita_immobiliare_id')->constrained('unita_immobiliari')->onDelete('cascade');
            $table->enum('tipo_accesso', ['proprietario', 'inquilino', 'amministratore']);
            $table->date('data_inizio')->nullable();
            $table->date('data_fine')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();
            
            $table->index(['user_id']);
            $table->index(['unita_immobiliare_id']);
            $table->index(['tipo_accesso']);
            $table->index(['is_active']);
            $table->unique(['user_id', 'unita_immobiliare_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('user_unita_immobiliari');
    }
};

4.4 Modelli Eloquent

Modello Stabile Completo

File: app/Models/Stabile.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;

class Stabile extends Model
{
    protected $fillable = [
        'denominazione',
        'indirizzo',
        'comune_id',
        'cap',
        'codice_fiscale',
        'partita_iva',
        'amministratore_id',
        'is_active',
        'note',
    ];

    protected $casts = [
        'is_active' => 'boolean',
    ];

    // Relazioni
    public function amministratore(): BelongsTo
    {
        return $this->belongsTo(User::class, 'amministratore_id');
    }

    public function comune(): BelongsTo
    {
        return $this->belongsTo(ComuneItaliano::class, 'comune_id');
    }

    public function unitaImmobiliari(): HasMany
    {
        return $this->hasMany(UnitaImmobiliare::class)->orderBy('numero');
    }

    public function documenti(): HasMany
    {
        return $this->hasMany(Documento::class)->orderBy('created_at', 'desc');
    }

    public function tickets(): HasMany
    {
        return $this->hasMany(Ticket::class)->orderBy('created_at', 'desc');
    }

    public function movimentiBancari(): HasMany
    {
        return $this->hasMany(MovimentoBancario::class)->orderBy('data_movimento', 'desc');
    }

    // Scope
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }

    public function scopeByAmministratore(Builder $query, int $amministratoreId): Builder
    {
        return $query->where('amministratore_id', $amministratoreId);
    }

    // Accessors
    public function getIndirizzoCompletoAttribute(): string
    {
        $indirizzo = $this->indirizzo;
        
        if ($this->comune) {
            $indirizzo .= ', ' . $this->comune->denominazione;
        }
        
        if ($this->cap) {
            $indirizzo .= ' (' . $this->cap . ')';
        }
        
        return $indirizzo;
    }

    public function getTotalUnitaAttribute(): int
    {
        return $this->unitaImmobiliari()->count();
    }

    public function getTotalMillesimiAttribute(): float
    {
        return $this->unitaImmobiliari()->sum('millesimi_proprieta') ?? 0.0;
    }

    public function getSaldoTotaleAttribute(): float
    {
        return $this->movimentiBancari()->sum('importo') ?? 0.0;
    }
}

Modello UnitaImmobiliare

File: app/Models/UnitaImmobiliare.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\Relations\BelongsToMany;

class UnitaImmobiliare extends Model
{
    protected $fillable = [
        'stabile_id',
        'numero',
        'piano',
        'scala',
        'interno',
        'tipo',
        'categoria_catastale',
        'classe_catastale',
        'consistenza',
        'superficie_catastale',
        'superficie_commerciale',
        'rendita_catastale',
        'millesimi_proprieta',
        'millesimi_riscaldamento',
        'millesimi_acqua',
        'millesimi_ascensore',
        'is_active',
        'note',
    ];

    protected $casts = [
        'superficie_catastale' => 'decimal:2',
        'superficie_commerciale' => 'decimal:2',
        'rendita_catastale' => 'decimal:2',
        'millesimi_proprieta' => 'decimal:4',
        'millesimi_riscaldamento' => 'decimal:4',
        'millesimi_acqua' => 'decimal:4',
        'millesimi_ascensore' => 'decimal:4',
        'is_active' => 'boolean',
    ];

    // Relazioni
    public function stabile(): BelongsTo
    {
        return $this->belongsTo(Stabile::class);
    }

    public function soggetti(): BelongsToMany
    {
        return $this->belongsToMany(Soggetto::class, 'diritti_reali')
            ->withPivot(['tipo_diritto', 'quota_proprieta', 'data_inizio', 'data_fine', 'is_active'])
            ->withTimestamps();
    }

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'user_unita_immobiliari')
            ->withPivot(['tipo_accesso', 'data_inizio', 'data_fine', 'is_active'])
            ->withTimestamps();
    }

    public function documenti(): HasMany
    {
        return $this->hasMany(Documento::class);
    }

    public function tickets(): HasMany
    {
        return $this->hasMany(Ticket::class);
    }

    public function ripartizioniSpese(): HasMany
    {
        return $this->hasMany(RipartizioneSpesa::class);
    }

    // Accessors
    public function getNumeroCompletoAttribute(): string
    {
        $numero = $this->numero;
        
        if ($this->piano) {
            $numero .= ' (Piano ' . $this->piano . ')';
        }
        
        if ($this->scala) {
            $numero .= ' - Scala ' . $this->scala;
        }
        
        if ($this->interno) {
            $numero .= ' - Interno ' . $this->interno;
        }
        
        return $numero;
    }

    public function getTipoDisplayAttribute(): string
    {
        $tipi = [
            'appartamento' => 'Appartamento',
            'negozio' => 'Negozio',
            'garage' => 'Garage',
            'cantina' => 'Cantina',
            'altro' => 'Altro',
        ];
        
        return $tipi[$this->tipo] ?? 'Non definito';
    }

    // Helper Methods
    public function getProprietari()
    {
        return $this->soggetti()->wherePivot('tipo_diritto', 'proprietario')
            ->wherePivot('is_active', true);
    }

    public function getInquilini()
    {
        return $this->soggetti()->wherePivot('tipo_diritto', 'inquilino')
            ->wherePivot('is_active', true);
    }
}

4.5 Seeder e Dati Base

Seeder Comuni Italiani

File: database/seeders/ComuniItalianiSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

class ComuniItalianiSeeder extends Seeder
{
    public function run(): void
    {
        // Verifica se i comuni sono già stati caricati
        if (DB::table('comuni_italiani')->count() > 0) {
            $this->command->info('Comuni italiani già presenti nel database');
            return;
        }

        // Carica da file CSV
        $csvFile = database_path('data/comuni_italiani.csv');
        
        if (!file_exists($csvFile)) {
            $this->command->error('File comuni_italiani.csv non trovato in database/data/');
            return;
        }

        $handle = fopen($csvFile, 'r');
        $header = fgetcsv($handle, 1000, ';');
        
        $comuni = [];
        while (($data = fgetcsv($handle, 1000, ';')) !== false) {
            $comuni[] = [
                'codice_catastale' => $data[0],
                'denominazione' => $data[1],
                'denominazione_tedesca' => $data[2] ?: null,
                'denominazione_altra' => $data[3] ?: null,
                'codice_provincia' => $data[4],
                'provincia' => $data[5],
                'regione' => $data[6],
                'cap' => $data[7] ?: null,
                'prefisso' => $data[8] ?: null,
                'email_pec' => $data[9] ?: null,
                'created_at' => now(),
                'updated_at' => now(),
            ];
        }
        
        fclose($handle);

        // Inserimento batch per performance
        $chunks = array_chunk($comuni, 1000);
        foreach ($chunks as $chunk) {
            DB::table('comuni_italiani')->insert($chunk);
        }

        $this->command->info('Caricati ' . count($comuni) . ' comuni italiani');
    }
}

Seeder Dati Demo

File: database/seeders/DemoDataSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Stabile;
use App\Models\UnitaImmobiliare;
use App\Models\Soggetto;
use App\Models\ComuneItaliano;

class DemoDataSeeder extends Seeder
{
    public function run(): void
    {
        // Trova comuni per demo
        $milano = ComuneItaliano::where('denominazione', 'Milano')->first();
        $roma = ComuneItaliano::where('denominazione', 'Roma')->first();
        
        if (!$milano || !$roma) {
            $this->command->error('Comuni Milano e Roma non trovati. Esegui prima ComuniItalianiSeeder');
            return;
        }

        // Crea utenti amministratore
        $admin1 = User::create([
            'name' => 'Admin Milano',
            'email' => 'admin.milano@netgescon.it',
            'password' => bcrypt('password'),
            'is_active' => true,
        ]);
        $admin1->assignRole('admin');

        $admin2 = User::create([
            'name' => 'Admin Roma',
            'email' => 'admin.roma@netgescon.it',
            'password' => bcrypt('password'),
            'is_active' => true,
        ]);
        $admin2->assignRole('amministratore');

        // Crea stabili
        $stabile1 = Stabile::create([
            'denominazione' => 'Condominio Via Roma 123',
            'indirizzo' => 'Via Roma 123',
            'comune_id' => $milano->id,
            'cap' => '20100',
            'codice_fiscale' => '80123456789',
            'amministratore_id' => $admin1->id,
            'is_active' => true,
        ]);

        $stabile2 = Stabile::create([
            'denominazione' => 'Residenza I Pini',
            'indirizzo' => 'Via dei Pini 45',
            'comune_id' => $roma->id,
            'cap' => '00100',
            'codice_fiscale' => '80987654321',
            'amministratore_id' => $admin2->id,
            'is_active' => true,
        ]);

        // Crea unità immobiliari
        for ($i = 1; $i <= 12; $i++) {
            UnitaImmobiliare::create([
                'stabile_id' => $stabile1->id,
                'numero' => (string) $i,
                'piano' => $i <= 4 ? '1' : ($i <= 8 ? '2' : '3'),
                'tipo' => 'appartamento',
                'superficie_catastale' => rand(60, 120),
                'millesimi_proprieta' => rand(70, 130) / 1000,
                'is_active' => true,
            ]);
        }

        for ($i = 1; $i <= 8; $i++) {
            UnitaImmobiliare::create([
                'stabile_id' => $stabile2->id,
                'numero' => (string) $i,
                'piano' => $i <= 4 ? '1' : '2',
                'tipo' => 'appartamento',
                'superficie_catastale' => rand(80, 150),
                'millesimi_proprieta' => rand(100, 180) / 1000,
                'is_active' => true,
            ]);
        }

        // Crea soggetti demo
        $soggetto1 = Soggetto::create([
            'tipo_soggetto' => 'persona_fisica',
            'nome' => 'Mario',
            'cognome' => 'Rossi',
            'codice_fiscale' => 'RSSMRA70A01H501Z',
            'indirizzo' => 'Via Roma 123',
            'comune_id' => $milano->id,
            'telefono' => '3331234567',
            'email' => 'mario.rossi@email.it',
            'is_active' => true,
        ]);

        $soggetto2 = Soggetto::create([
            'tipo_soggetto' => 'persona_fisica',
            'nome' => 'Anna',
            'cognome' => 'Verdi',
            'codice_fiscale' => 'VRDNNA75B01F205X',
            'indirizzo' => 'Via dei Pini 45',
            'comune_id' => $roma->id,
            'telefono' => '3339876543',
            'email' => 'anna.verdi@email.it',
            'is_active' => true,
        ]);

        // Collega soggetti a unità immobiliari
        $unita1 = UnitaImmobiliare::where('stabile_id', $stabile1->id)->first();
        $unita2 = UnitaImmobiliare::where('stabile_id', $stabile2->id)->first();

        $unita1->soggetti()->attach($soggetto1->id, [
            'tipo_diritto' => 'proprietario',
            'quota_proprieta' => 1.0000,
            'data_inizio' => now()->subYear(),
            'is_active' => true,
        ]);

        $unita2->soggetti()->attach($soggetto2->id, [
            'tipo_diritto' => 'proprietario',
            'quota_proprieta' => 1.0000,
            'data_inizio' => now()->subYear(),
            'is_active' => true,
        ]);

        $this->command->info('Dati demo creati con successo');
    }
}

4.6 Gestione Conflitti Migrazioni

Comando Verifica Stato Database

File: app/Console/Commands/CheckDatabaseStatus.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class CheckDatabaseStatus extends Command
{
    protected $signature = 'db:check-status';
    protected $description = 'Verifica lo stato del database e rileva possibili conflitti';

    public function handle()
    {
        $this->info('🔍 Verifica stato database NetGescon...');
        
        // Verifica connessione database
        try {
            DB::connection()->getPdo();
            $this->info('✅ Connessione database OK');
        } catch (\Exception $e) {
            $this->error('❌ Errore connessione database: ' . $e->getMessage());
            return 1;
        }

        // Verifica tabelle principali
        $requiredTables = [
            'users', 'roles', 'permissions', 'model_has_roles',
            'comuni_italiani', 'stabili', 'soggetti', 'unita_immobiliari',
            'diritti_reali', 'documenti', 'tickets', 'movimenti_bancari'
        ];

        $this->info('📋 Verifica tabelle principali...');
        foreach ($requiredTables as $table) {
            if (Schema::hasTable($table)) {
                $count = DB::table($table)->count();
                $this->info("✅ {$table}: {$count} record");
            } else {
                $this->warn("⚠️  {$table}: TABELLA MANCANTE");
            }
        }

        // Verifica migrazioni
        $this->info('🔄 Verifica migrazioni...');
        $pendingMigrations = DB::table('migrations')
            ->pluck('migration')
            ->toArray();

        if (empty($pendingMigrations)) {
            $this->warn('⚠️  Nessuna migrazione eseguita');
        } else {
            $this->info('✅ Migrazioni eseguite: ' . count($pendingMigrations));
        }

        // Verifica conflitti campi
        $this->info('⚡ Verifica conflitti campi...');
        $this->checkTableConflicts();

        // Verifica indici
        $this->info('📊 Verifica indici database...');
        $this->checkIndexes();

        $this->info('✅ Verifica completata!');
        return 0;
    }

    private function checkTableConflicts()
    {
        $conflicts = [];

        // Verifica campi duplicati in users
        if (Schema::hasTable('users')) {
            $columns = Schema::getColumnListing('users');
            $expectedColumns = ['id', 'name', 'email', 'password', 'is_active', 'last_login_at'];
            
            foreach ($expectedColumns as $col) {
                if (!in_array($col, $columns)) {
                    $conflicts[] = "users.{$col} mancante";
                }
            }
        }

        // Verifica campi stabili
        if (Schema::hasTable('stabili')) {
            $columns = Schema::getColumnListing('stabili');
            if (!in_array('amministratore_id', $columns)) {
                $conflicts[] = 'stabili.amministratore_id mancante';
            }
            if (!in_array('is_active', $columns)) {
                $conflicts[] = 'stabili.is_active mancante';
            }
        }

        if (!empty($conflicts)) {
            $this->error('❌ Conflitti rilevati:');
            foreach ($conflicts as $conflict) {
                $this->error("  - {$conflict}");
            }
        } else {
            $this->info('✅ Nessun conflitto rilevato');
        }
    }

    private function checkIndexes()
    {
        $tables = ['users', 'stabili', 'unita_immobiliari', 'soggetti'];
        
        foreach ($tables as $table) {
            if (Schema::hasTable($table)) {
                $indexes = collect(DB::select("SHOW INDEX FROM {$table}"))
                    ->pluck('Key_name')
                    ->unique()
                    ->values()
                    ->toArray();
                
                $this->info("  {$table}: " . count($indexes) . " indici");
            }
        }
    }
}

Comando Reset Database

File: app/Console/Commands/ResetDatabase.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan;

class ResetDatabase extends Command
{
    protected $signature = 'db:reset-clean {--force}';
    protected $description = 'Reset completo database con pulizia conflitti';

    public function handle()
    {
        if (!$this->option('force')) {
            if (!$this->confirm('⚠️  Questa operazione cancellerà TUTTI i dati. Continuare?')) {
                $this->info('Operazione annullata');
                return 0;
            }
        }

        $this->info('🔄 Inizio reset database...');

        // Drop tutte le tabelle
        $this->info('🗑️  Rimozione tabelle esistenti...');
        $this->dropAllTables();

        // Pulisci tabella migrazioni
        $this->info('🧹 Pulizia migrazioni...');
        try {
            DB::statement('DROP TABLE IF EXISTS migrations');
        } catch (\Exception $e) {
            // Ignore se non esiste
        }

        // Ricrea tutto
        $this->info('⚡ Ricreazione database...');
        Artisan::call('migrate:fresh');

        $this->info('🌱 Esecuzione seeder...');
        Artisan::call('db:seed');

        $this->info('✅ Reset database completato!');
        return 0;
    }

    private function dropAllTables()
    {
        $tables = DB::select('SHOW TABLES');
        $dbName = DB::getDatabaseName();
        
        DB::statement('SET FOREIGN_KEY_CHECKS = 0');
        
        foreach ($tables as $table) {
            $tableName = $table->{"Tables_in_{$dbName}"};
            DB::statement("DROP TABLE IF EXISTS {$tableName}");
        }
        
        DB::statement('SET FOREIGN_KEY_CHECKS = 1');
    }
}

4.7 Triggers e Codici Univoci

4.7.1 Sistema Codici Univoci

Il sistema NetGescon utilizza codici univoci alfanumerici di 8 caratteri per identificare univocamente utenti, amministratori e altre entità critiche.

Caratteristiche:

  • Lunghezza: 8 caratteri
  • Formato: Alfanumerico (A-Z, 0-9)
  • Unicità: Garantita tramite trigger SQL
  • Generazione: Automatica all'inserimento

4.7.2 Trigger per Amministratori

-- Trigger per generare codice_univoco automaticamente per amministratori
DELIMITER $$

CREATE TRIGGER generate_codice_univoco_amministratori
BEFORE INSERT ON amministratori
FOR EACH ROW
BEGIN
    DECLARE codice_temp VARCHAR(8);
    DECLARE codice_exists INT DEFAULT 1;
    
    -- Solo se il codice non è già fornito
    IF NEW.codice_univoco IS NULL OR NEW.codice_univoco = '' THEN
        WHILE codice_exists > 0 DO
            SET codice_temp = CONCAT(
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1)
            );
            
            SELECT COUNT(*) INTO codice_exists 
            FROM amministratori 
            WHERE codice_univoco = codice_temp;
        END WHILE;
        
        SET NEW.codice_univoco = codice_temp;
    END IF;
END$$

DELIMITER ;

4.7.3 Trigger per Users

-- Trigger per generare codice_univoco per users
DELIMITER $$

CREATE TRIGGER generate_codice_univoco_users
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
    DECLARE codice_temp VARCHAR(8);
    DECLARE codice_exists INT DEFAULT 1;
    
    -- Solo se il codice non è già fornito
    IF NEW.codice_univoco IS NULL OR NEW.codice_univoco = '' THEN
        WHILE codice_exists > 0 DO
            SET codice_temp = CONCAT(
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', FLOOR(1 + (RAND() * 36)), 1)
            );
            
            SELECT COUNT(*) INTO codice_exists 
            FROM users 
            WHERE codice_univoco = codice_temp;
        END WHILE;
        
        SET NEW.codice_univoco = codice_temp;
    END IF;
END$$

DELIMITER ;

4.7.4 Migration Laravel per Triggers

<?php
// database/migrations/2025_07_17_210000_create_codice_univoco_triggers.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up(): void
    {
        // Trigger per amministratori
        DB::unprepared('
            DROP TRIGGER IF EXISTS generate_codice_univoco_amministratori;
            
            DELIMITER $$
            CREATE TRIGGER generate_codice_univoco_amministratori
            BEFORE INSERT ON amministratori
            FOR EACH ROW
            BEGIN
                DECLARE codice_temp VARCHAR(8);
                DECLARE codice_exists INT DEFAULT 1;
                
                IF NEW.codice_univoco IS NULL OR NEW.codice_univoco = "" THEN
                    WHILE codice_exists > 0 DO
                        SET codice_temp = CONCAT(
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1),
                            SUBSTRING("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", FLOOR(1 + (RAND() * 36)), 1)
                        );
                        
                        SELECT COUNT(*) INTO codice_exists 
                        FROM amministratori 
                        WHERE codice_univoco = codice_temp;
                    END WHILE;
                    
                    SET NEW.codice_univoco = codice_temp;
                END IF;
            END$$
            DELIMITER ;
        ');
    }

    public function down(): void
    {
        DB::unprepared('DROP TRIGGER IF EXISTS generate_codice_univoco_amministratori');
    }
};

4.7.5 Vantaggi del Sistema

Automazione Completa

  • Generazione automatica senza intervento manuale
  • Nessun rischio di dimenticanza o errore umano

Sicurezza

  • Codici difficili da indovinare
  • Distribuzione uniforme dei caratteri
  • Impossibilità di duplicati

Performance

  • Operazione a livello database
  • Nessun overhead applicativo
  • Controllo di unicità ottimizzato

Manutenibilità

  • Trigger gestiti tramite migrations
  • Versionamento del codice
  • Rollback automatico

4.10 Sincronizzazione Multi-Macchina

4.10.1 Architettura di Sincronizzazione

NetGescon supporta la sincronizzazione automatica tra più macchine per garantire consistenza e aggiornamenti distribuiti.

Componenti:

  • Macchina di Sviluppo: Windows WSL (locale)
  • Macchina Master: Linux Ubuntu 24.04 (192.168.0.43)
  • Macchine Client: Istanze replicate del sistema

4.10.2 Script di Sincronizzazione

Script PowerShell (Windows)

# netgescon-sync.ps1 - Script di sincronizzazione Windows
$LOCAL_PROJECT_PATH = "U:\home\michele\netgescon\netgescon-laravel"
$REMOTE_HOST = "192.168.0.43"
$REMOTE_USER = "michele"
$REMOTE_PATH = "/var/www/netgescon"

# Funzione di sincronizzazione migrations
function Sync-Migrations {
    Write-Log "Sincronizzando migrations..."
    scp -r "$LOCAL_PROJECT_PATH\database\migrations\*" "$REMOTE_USER@$REMOTE_HOST`:$REMOTE_PATH/database/migrations/"
    
    if ($LASTEXITCODE -eq 0) {
        Write-Log "Migrations sincronizzate" "SUCCESS"
        return $true
    } else {
        Write-Log "Errore sincronizzazione migrations" "ERROR"
        return $false
    }
}

# Esecuzione migrations remote
function Invoke-RemoteMigrations {
    Write-Log "Eseguo migrations remote..."
    ssh "$REMOTE_USER@$REMOTE_HOST" "cd $REMOTE_PATH && php artisan migrate --force"
    
    if ($LASTEXITCODE -eq 0) {
        Write-Log "Migrations remote eseguite" "SUCCESS"
        return $true
    } else {
        Write-Log "Errore esecuzione migrations remote" "ERROR"
        return $false
    }
}

Script Bash (Linux)

#!/bin/bash
# netgescon-sync.sh - Script di sincronizzazione Linux

LOCAL_PROJECT_PATH="/mnt/u/home/michele/netgescon/netgescon-laravel"
REMOTE_HOST="192.168.0.43"
REMOTE_USER="michele"
REMOTE_PATH="/var/www/netgescon"

# Sincronizza migrations
sync_migrations() {
    info "Sincronizzando migrations..."
    rsync -avz --delete \
        "$LOCAL_PROJECT_PATH/database/migrations/" \
        "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/database/migrations/"
    
    if [ $? -eq 0 ]; then
        log "✅ Migrations sincronizzate"
        return 0
    else
        error "❌ Errore sincronizzazione migrations"
        return 1
    fi
}

# Esegui migrations remote
run_remote_migrations() {
    info "Eseguo migrations remote..."
    ssh "$REMOTE_USER@$REMOTE_HOST" "
        cd $REMOTE_PATH
        php artisan migrate --force
    "
    
    if [ $? -eq 0 ]; then
        log "✅ Migrations remote eseguite"
        return 0
    else
        error "❌ Errore esecuzione migrations remote"
        return 1
    fi
}

4.10.3 Flusso di Sincronizzazione

  1. Sviluppo Locale (Windows WSL)
  2. SincronizzazioneMacchina Master (192.168.0.43)
  3. DistribuzioneMacchine Client (altre istanze)

4.10.4 Comandi di Utilizzo

# Avvia script di sincronizzazione
./netgescon-sync.sh

# Opzioni disponibili:
# 1. Sincronizza solo migrations
# 2. Sincronizza migrations + seeders
# 3. Sincronizza + esegui migrations
# 4. Sincronizza + esegui migrations + seeding
# 5. Full sync + restart services
# Avvia script PowerShell
.\netgescon-sync.ps1

# Menu interattivo con opzioni di sincronizzazione
# Logging automatico in C:\temp\netgescon_sync_*.log

4.10.5 Backup Automatico

Ogni sincronizzazione include:

  • Backup automatico della macchina remota
  • Logging dettagliato di tutte le operazioni
  • Rollback capability in caso di errori
  • Verifica connessione prima di ogni operazione

4.8 Backup e Ripristino

Script Backup Automatico

File: scripts/backup-database.sh

#!/bin/bash

# Configurazione
DB_NAME="netgescon_db"
DB_USER="netgescon"
DB_PASS="netgescon2024"
BACKUP_DIR="/var/backups/netgescon"
DATE=$(date +%Y%m%d_%H%M%S)

# Crea directory backup
mkdir -p $BACKUP_DIR

# Backup completo
echo "🗄️  Backup database $DB_NAME..."
mysqldump -u $DB_USER -p$DB_PASS $DB_NAME > $BACKUP_DIR/netgescon_$DATE.sql

# Backup solo struttura
mysqldump -u $DB_USER -p$DB_PASS --no-data $DB_NAME > $BACKUP_DIR/netgescon_structure_$DATE.sql

# Backup solo dati
mysqldump -u $DB_USER -p$DB_PASS --no-create-info $DB_NAME > $BACKUP_DIR/netgescon_data_$DATE.sql

# Comprimi backup
gzip $BACKUP_DIR/netgescon_$DATE.sql
gzip $BACKUP_DIR/netgescon_structure_$DATE.sql
gzip $BACKUP_DIR/netgescon_data_$DATE.sql

# Rimuovi backup più vecchi di 7 giorni
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete

echo "✅ Backup completato: $BACKUP_DIR/netgescon_$DATE.sql.gz"

Script Ripristino

File: scripts/restore-database.sh

#!/bin/bash

if [ $# -eq 0 ]; then
    echo "Uso: $0 <file_backup.sql>"
    echo "Esempio: $0 /var/backups/netgescon/netgescon_20240117_120000.sql.gz"
    exit 1
fi

BACKUP_FILE=$1
DB_NAME="netgescon_db"
DB_USER="netgescon"
DB_PASS="netgescon2024"

if [ ! -f "$BACKUP_FILE" ]; then
    echo "❌ File backup non trovato: $BACKUP_FILE"
    exit 1
fi

# Conferma operazione
echo "⚠️  Ripristino database da: $BACKUP_FILE"
read -p "Questa operazione sovrascriverà i dati esistenti. Continuare? (y/N): " -n 1 -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    echo "Operazione annullata"
    exit 0
fi

# Decomprimi se necessario
if [[ $BACKUP_FILE == *.gz ]]; then
    echo "📦 Decompressione backup..."
    gunzip -c $BACKUP_FILE > /tmp/restore_temp.sql
    RESTORE_FILE="/tmp/restore_temp.sql"
else
    RESTORE_FILE=$BACKUP_FILE
fi

# Ripristino
echo "🔄 Ripristino database..."
mysql -u $DB_USER -p$DB_PASS $DB_NAME < $RESTORE_FILE

# Pulizia
if [ -f "/tmp/restore_temp.sql" ]; then
    rm /tmp/restore_temp.sql
fi

echo "✅ Ripristino completato!"

📝 COMPLETATO: Capitolo 4 - Database e Strutture

Questo capitolo fornisce una guida completa per:

  • Schema database completo con tutte le tabelle
  • Migrazioni Laravel strutturate
  • Relazioni e vincoli foreign key
  • Modelli Eloquent con relazioni
  • Seeder per dati base e demo
  • Gestione conflitti migrazioni
  • Troubleshooting errori comuni
  • Backup e ripristino automatico

🎯 Questo capitolo risolve il problema segnalato dei conflitti nelle migrazioni fornendo:

  • Comandi di verifica stato database
  • Script di reset pulito
  • Troubleshooting per errori comuni
  • Backup automatico prima di operazioni rischiose

🔄 Prossimo capitolo da completare: API e Integrazioni