netgescon-master/docs/specifiche/DOCUMENT_MANAGEMENT_SYSTEM.md
2025-07-20 14:57:25 +00:00

23 KiB

📁 NetGesCon Laravel - Sistema Gestione Documentale

📍 OVERVIEW GENERALE

Sistema integrato per gestione documenti condominiali con supporto per archiviazione locale, cloud (Office 365, Google Drive) e audit documentale completo.


🏗️ ARCHITETTURA ARCHIVIAZIONE DOCUMENTI

1. Struttura Database Documenti

A. Tabella Principale Documenti

-- Migration: create_documenti_table.php
CREATE TABLE documenti (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    stabile_id BIGINT UNSIGNED NOT NULL,
    amministratore_id BIGINT UNSIGNED NOT NULL,
    categoria_documento ENUM('contratto', 'fattura', 'verbale', 'corrispondenza', 'tecnico', 'legale', 'assicurazione', 'altro') NOT NULL,
    tipo_documento VARCHAR(100) NOT NULL,
    titolo VARCHAR(255) NOT NULL,
    descrizione TEXT,
    nome_file VARCHAR(255) NOT NULL,
    nome_file_originale VARCHAR(255) NOT NULL,
    path_relativo VARCHAR(500) NOT NULL,
    path_assoluto VARCHAR(1000) NOT NULL,
    dimensione_file BIGINT UNSIGNED NOT NULL,
    mime_type VARCHAR(100) NOT NULL,
    hash_file VARCHAR(64) NOT NULL, -- SHA256 per integrità
    stato_documento ENUM('bozza', 'attivo', 'archiviato', 'eliminato') DEFAULT 'attivo',
    data_documento DATE,
    data_scadenza DATE NULL,
    numero_protocollo VARCHAR(50) NULL,
    anno_protocollo YEAR NULL,
    note_interne TEXT,
    metadati_personalizzati JSON,
    visibilita ENUM('privato', 'amministratore', 'condomini', 'pubblico') DEFAULT 'amministratore',
    -- Audit fields
    caricato_da BIGINT UNSIGNED NOT NULL,
    modificato_da BIGINT UNSIGNED NULL,
    verificato_da BIGINT UNSIGNED NULL,
    verificato_at TIMESTAMP NULL,
    -- Cloud sync
    sincronizzato_cloud BOOLEAN DEFAULT FALSE,
    cloud_provider ENUM('office365', 'google_drive', 'dropbox') NULL,
    cloud_file_id VARCHAR(255) NULL,
    cloud_ultimo_sync TIMESTAMP NULL,
    -- Timestamps
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL,
    
    -- Indexes
    INDEX idx_stabile_categoria (stabile_id, categoria_documento),
    INDEX idx_data_documento (data_documento),
    INDEX idx_data_scadenza (data_scadenza),
    INDEX idx_stato (stato_documento),
    INDEX idx_hash (hash_file),
    INDEX idx_protocollo (numero_protocollo, anno_protocollo),
    
    -- Foreign Keys
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    FOREIGN KEY (amministratore_id) REFERENCES amministratori(id) ON DELETE CASCADE,
    FOREIGN KEY (caricato_da) REFERENCES users(id),
    FOREIGN KEY (modificato_da) REFERENCES users(id),
    FOREIGN KEY (verificato_da) REFERENCES users(id)
);

B. Tabella Collegamenti Documenti

-- Per collegare documenti a voci di spesa, ripartizioni, etc.
CREATE TABLE collegamenti_documenti (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    documento_id BIGINT UNSIGNED NOT NULL,
    entita_tipo VARCHAR(50) NOT NULL, -- 'voce_spesa', 'ripartizione_spese', 'rata', etc.
    entita_id BIGINT UNSIGNED NOT NULL,
    tipo_collegamento ENUM('supporto', 'fattura', 'ricevuta', 'autorizzazione', 'altro') NOT NULL,
    note VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_documento (documento_id),
    INDEX idx_entita (entita_tipo, entita_id),
    
    FOREIGN KEY (documento_id) REFERENCES documenti(id) ON DELETE CASCADE
);

C. Tabella Versioni Documenti

CREATE TABLE versioni_documenti (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    documento_id BIGINT UNSIGNED NOT NULL,
    numero_versione INT NOT NULL DEFAULT 1,
    nome_file VARCHAR(255) NOT NULL,
    path_relativo VARCHAR(500) NOT NULL,
    dimensione_file BIGINT UNSIGNED NOT NULL,
    hash_file VARCHAR(64) NOT NULL,
    modifiche_descrizione TEXT,
    creato_da BIGINT UNSIGNED NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_documento_versione (documento_id, numero_versione),
    
    FOREIGN KEY (documento_id) REFERENCES documenti(id) ON DELETE CASCADE,
    FOREIGN KEY (creato_da) REFERENCES users(id)
);

🗂️ STRUTTURA CARTELLE FISICHE

1. Organizzazione Filesystem

storage/app/
├── documenti/
│   ├── amministratore_{id}/
│   │   ├── stabile_{id}/
│   │   │   ├── {anno}/
│   │   │   │   ├── contratti/
│   │   │   │   ├── fatture/
│   │   │   │   │   ├── {mese}/
│   │   │   │   │   │   ├── fornitori/
│   │   │   │   │   │   └── utenze/
│   │   │   │   ├── verbali/
│   │   │   │   │   ├── assemblee/
│   │   │   │   │   └── consiglio/
│   │   │   │   ├── corrispondenza/
│   │   │   │   │   ├── ingresso/
│   │   │   │   │   └── uscita/
│   │   │   │   ├── tecnici/
│   │   │   │   │   ├── progetti/
│   │   │   │   │   ├── certificati/
│   │   │   │   │   └── collaudi/
│   │   │   │   ├── legali/
│   │   │   │   ├── assicurazioni/
│   │   │   │   └── altro/
│   │   │   └── versioni/
│   │   │       └── {documento_id}/
│   │   └── backup/
│   │       └── {data}/
│   └── templates/
│       ├── contratti/
│       ├── lettere/
│       └── verbali/

2. Naming Convention

Formato: {YYYY}{MM}{DD}_{categoria}_{progressivo}_{descrizione_breve}.{ext}
Esempio: 20250127_fattura_001_enel_energia_elettrica.pdf
         20250127_verbale_001_assemblea_ordinaria.pdf
         20250127_contratto_001_pulizie_ditta_abc.pdf

🔐 MODELLO ELOQUENT E SERVIZI

1. Modello Documento

// app/Models/Documento.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;

class Documento extends Model
{
    use SoftDeletes;

    protected $table = 'documenti';

    protected $fillable = [
        'stabile_id', 'amministratore_id', 'categoria_documento', 
        'tipo_documento', 'titolo', 'descrizione', 'nome_file',
        'nome_file_originale', 'path_relativo', 'dimensione_file',
        'mime_type', 'hash_file', 'stato_documento', 'data_documento',
        'data_scadenza', 'numero_protocollo', 'anno_protocollo',
        'note_interne', 'metadati_personalizzati', 'visibilita',
        'caricato_da'
    ];

    protected $casts = [
        'data_documento' => 'date',
        'data_scadenza' => 'date',
        'metadati_personalizzati' => 'array',
        'sincronizzato_cloud' => 'boolean',
        'verificato_at' => 'datetime',
        'cloud_ultimo_sync' => 'datetime'
    ];

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

    public function amministratore()
    {
        return $this->belongsTo(Amministratore::class);
    }

    public function caricatoDa()
    {
        return $this->belongsTo(User::class, 'caricato_da');
    }

    public function modificatoDa()
    {
        return $this->belongsTo(User::class, 'modificato_da');
    }

    public function verificatoDa()
    {
        return $this->belongsTo(User::class, 'verificato_da');
    }

    public function versioni()
    {
        return $this->hasMany(VersioneDocumento::class);
    }

    public function collegamenti()
    {
        return $this->hasMany(CollegamentoDocumento::class);
    }

    // Scopes
    public function scopePerStabile($query, $stabileId)
    {
        return $query->where('stabile_id', $stabileId);
    }

    public function scopePerCategoria($query, $categoria)
    {
        return $query->where('categoria_documento', $categoria);
    }

    public function scopeInScadenza($query, $giorni = 30)
    {
        return $query->whereNotNull('data_scadenza')
                    ->whereBetween('data_scadenza', [
                        now()->toDateString(),
                        now()->addDays($giorni)->toDateString()
                    ]);
    }

    public function scopeAttivi($query)
    {
        return $query->where('stato_documento', 'attivo');
    }

    // Metodi utilità
    public function getPathCompletoAttribute()
    {
        return Storage::path($this->path_relativo);
    }

    public function getUrlDownloadAttribute()
    {
        return route('documenti.download', $this->id);
    }

    public function getDimensioneFormattataAttribute()
    {
        return $this->formatBytes($this->dimensione_file);
    }

    private function formatBytes($size)
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $i = 0;
        while ($size >= 1024 && $i < count($units) - 1) {
            $size /= 1024;
            $i++;
        }
        return round($size, 2) . ' ' . $units[$i];
    }

    public function verificaIntegrita()
    {
        if (!Storage::exists($this->path_relativo)) {
            return false;
        }
        
        $hashCorrente = hash_file('sha256', Storage::path($this->path_relativo));
        return $hashCorrente === $this->hash_file;
    }

    public function creaVersione($motivo = null)
    {
        return VersioneDocumento::create([
            'documento_id' => $this->id,
            'numero_versione' => $this->versioni()->max('numero_versione') + 1,
            'nome_file' => $this->nome_file,
            'path_relativo' => $this->path_relativo,
            'dimensione_file' => $this->dimensione_file,
            'hash_file' => $this->hash_file,
            'modifiche_descrizione' => $motivo,
            'creato_da' => auth()->id()
        ]);
    }

    // Categorie statiche
    public static function getCategorie()
    {
        return [
            'contratto' => 'Contratti',
            'fattura' => 'Fatture',
            'verbale' => 'Verbali',
            'corrispondenza' => 'Corrispondenza',
            'tecnico' => 'Documenti Tecnici',
            'legale' => 'Documenti Legali',
            'assicurazione' => 'Assicurazioni',
            'altro' => 'Altro'
        ];
    }

    public static function getTipiVisibilita()
    {
        return [
            'privato' => 'Solo Amministratore',
            'amministratore' => 'Staff Amministrazione',
            'condomini' => 'Condomini',
            'pubblico' => 'Pubblico'
        ];
    }
}

2. Servizio Gestione Documenti

// app/Services/DocumentoService.php
<?php

namespace App\Services;

use App\Models\Documento;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class DocumentoService
{
    public function caricaDocumento(UploadedFile $file, array $dati)
    {
        // Validazione file
        $this->validaFile($file);
        
        // Genera path di destinazione
        $pathRelativo = $this->generaPath($dati);
        
        // Calcola hash per integrità
        $hashFile = hash_file('sha256', $file->getPathname());
        
        // Verifica duplicati
        if ($this->verificaDuplicato($hashFile, $dati['stabile_id'])) {
            throw new \Exception('Il documento è già presente nell\'archivio');
        }
        
        // Genera nome file univoco
        $nomeFile = $this->generaNomeFile($file, $dati);
        $pathCompleto = $pathRelativo . '/' . $nomeFile;
        
        // Salva il file
        $file->storeAs(dirname($pathCompleto), basename($pathCompleto));
        
        // Crea record in database
        return Documento::create([
            'stabile_id' => $dati['stabile_id'],
            'amministratore_id' => $dati['amministratore_id'],
            'categoria_documento' => $dati['categoria_documento'],
            'tipo_documento' => $dati['tipo_documento'],
            'titolo' => $dati['titolo'],
            'descrizione' => $dati['descrizione'] ?? null,
            'nome_file' => $nomeFile,
            'nome_file_originale' => $file->getClientOriginalName(),
            'path_relativo' => $pathCompleto,
            'path_assoluto' => Storage::path($pathCompleto),
            'dimensione_file' => $file->getSize(),
            'mime_type' => $file->getMimeType(),
            'hash_file' => $hashFile,
            'data_documento' => $dati['data_documento'] ?? now()->toDateString(),
            'data_scadenza' => $dati['data_scadenza'] ?? null,
            'numero_protocollo' => $dati['numero_protocollo'] ?? null,
            'anno_protocollo' => $dati['anno_protocollo'] ?? now()->year,
            'note_interne' => $dati['note_interne'] ?? null,
            'metadati_personalizzati' => $dati['metadati'] ?? [],
            'visibilita' => $dati['visibilita'] ?? 'amministratore',
            'caricato_da' => auth()->id()
        ]);
    }

    private function generaPath(array $dati)
    {
        $anno = $dati['anno'] ?? now()->year;
        $mese = $dati['mese'] ?? now()->format('m');
        
        return "documenti/amministratore_{$dati['amministratore_id']}/stabile_{$dati['stabile_id']}/{$anno}/{$dati['categoria_documento']}" . 
               ($dati['categoria_documento'] === 'fattura' ? "/{$mese}" : '');
    }

    private function generaNomeFile(UploadedFile $file, array $dati)
    {
        $data = now()->format('Ymd');
        $categoria = $dati['categoria_documento'];
        $progressivo = $this->getProgressivo($dati['stabile_id'], $categoria);
        $descrizione = Str::slug($dati['descrizione_breve'] ?? 'documento');
        $estensione = $file->getClientOriginalExtension();
        
        return "{$data}_{$categoria}_{$progressivo}_{$descrizione}.{$estensione}";
    }

    private function getProgressivo($stabileId, $categoria)
    {
        $ultimo = Documento::where('stabile_id', $stabileId)
                           ->where('categoria_documento', $categoria)
                           ->whereDate('created_at', now()->toDateString())
                           ->count();
        
        return str_pad($ultimo + 1, 3, '0', STR_PAD_LEFT);
    }

    private function validaFile(UploadedFile $file)
    {
        $tipiConsentiti = [
            'application/pdf',
            'image/jpeg',
            'image/png',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        ];
        
        if (!in_array($file->getMimeType(), $tipiConsentiti)) {
            throw new \Exception('Tipo di file non consentito');
        }
        
        if ($file->getSize() > 50 * 1024 * 1024) { // 50MB
            throw new \Exception('File troppo grande (max 50MB)');
        }
    }

    private function verificaDuplicato($hash, $stabileId)
    {
        return Documento::where('hash_file', $hash)
                       ->where('stabile_id', $stabileId)
                       ->where('stato_documento', 'attivo')
                       ->exists();
    }
}

☁️ INTEGRAZIONE CLOUD STORAGE

1. Servizio Office 365

// app/Services/Office365Service.php
<?php

namespace App\Services;

use Microsoft\Graph\Graph;
use Microsoft\Graph\Model;

class Office365Service
{
    private $graph;

    public function __construct()
    {
        $this->graph = new Graph();
        $this->graph->setAccessToken($this->getAccessToken());
    }

    public function sincronizzaDocumento(Documento $documento)
    {
        try {
            // Crea cartella se non esiste
            $cartellaPadre = $this->creaCatellaStabile($documento->stabile);
            
            // Upload del file
            $fileContent = Storage::get($documento->path_relativo);
            $uploadedFile = $this->graph->createRequest('PUT', 
                "/me/drive/items/{$cartellaPadre}/children/{$documento->nome_file}/content")
                ->attachBody($fileContent)
                ->execute();

            // Aggiorna documento con info cloud
            $documento->update([
                'sincronizzato_cloud' => true,
                'cloud_provider' => 'office365',
                'cloud_file_id' => $uploadedFile->getId(),
                'cloud_ultimo_sync' => now()
            ]);

            return true;
        } catch (\Exception $e) {
            \Log::error('Errore sincronizzazione Office 365: ' . $e->getMessage());
            return false;
        }
    }

    private function creaCatellaStabile($stabile)
    {
        $nomeCartella = "Stabile_{$stabile->codice}_{$stabile->denominazione}";
        
        // Verifica se esiste
        $folders = $this->graph->createRequest('GET', 
            "/me/drive/root/children?filter=name eq '{$nomeCartella}'")
            ->execute();

        if ($folders->getBody()['value']) {
            return $folders->getBody()['value'][0]['id'];
        }

        // Crea nuova cartella
        $folderData = [
            'name' => $nomeCartella,
            'folder' => new \stdClass()
        ];

        $newFolder = $this->graph->createRequest('POST', '/me/drive/root/children')
            ->attachBody($folderData)
            ->execute();

        return $newFolder->getId();
    }

    private function getAccessToken()
    {
        // Implementa OAuth2 flow per Office 365
        // Restituisce access token valido
    }
}

2. Servizio Google Drive

// app/Services/GoogleDriveService.php
<?php

namespace App\Services;

use Google_Client;
use Google_Service_Drive;
use Google_Service_Drive_DriveFile;

class GoogleDriveService
{
    private $client;
    private $service;

    public function __construct()
    {
        $this->client = new Google_Client();
        $this->client->setClientId(config('services.google.client_id'));
        $this->client->setClientSecret(config('services.google.client_secret'));
        $this->client->setRedirectUri(config('services.google.redirect_uri'));
        $this->client->addScope(Google_Service_Drive::DRIVE);
        
        $this->service = new Google_Service_Drive($this->client);
    }

    public function sincronizzaDocumento(Documento $documento)
    {
        try {
            // Crea cartella se non esiste
            $cartellaPadre = $this->creaCatellaStabile($documento->stabile);
            
            // Prepara file per upload
            $fileMetadata = new Google_Service_Drive_DriveFile([
                'name' => $documento->nome_file,
                'parents' => [$cartellaPadre]
            ]);
            
            $content = Storage::get($documento->path_relativo);
            
            $file = $this->service->files->create($fileMetadata, [
                'data' => $content,
                'mimeType' => $documento->mime_type,
                'uploadType' => 'multipart'
            ]);

            // Aggiorna documento
            $documento->update([
                'sincronizzato_cloud' => true,
                'cloud_provider' => 'google_drive',
                'cloud_file_id' => $file->id,
                'cloud_ultimo_sync' => now()
            ]);

            return true;
        } catch (\Exception $e) {
            \Log::error('Errore sincronizzazione Google Drive: ' . $e->getMessage());
            return false;
        }
    }

    private function creaCatellaStabile($stabile)
    {
        $nomeCartella = "Stabile_{$stabile->codice}_{$stabile->denominazione}";
        
        // Cerca cartella esistente
        $response = $this->service->files->listFiles([
            'q' => "name='{$nomeCartella}' and mimeType='application/vnd.google-apps.folder'",
            'spaces' => 'drive'
        ]);

        if (count($response->files) > 0) {
            return $response->files[0]->id;
        }

        // Crea nuova cartella
        $fileMetadata = new Google_Service_Drive_DriveFile([
            'name' => $nomeCartella,
            'mimeType' => 'application/vnd.google-apps.folder'
        ]);

        $folder = $this->service->files->create($fileMetadata);
        return $folder->id;
    }
}

🔍 SISTEMA AUDIT DOCUMENTALE

1. Modello Audit Log

// app/Models/AuditDocumento.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class AuditDocumento extends Model
{
    protected $table = 'audit_documenti';

    protected $fillable = [
        'documento_id', 'utente_id', 'azione', 'dettagli',
        'ip_address', 'user_agent', 'dati_precedenti', 'dati_nuovi'
    ];

    protected $casts = [
        'dettagli' => 'array',
        'dati_precedenti' => 'array',
        'dati_nuovi' => 'array'
    ];

    public function documento()
    {
        return $this->belongsTo(Documento::class);
    }

    public function utente()
    {
        return $this->belongsTo(User::class);
    }

    public static function logAzione($documento, $azione, $dettagli = [])
    {
        return self::create([
            'documento_id' => $documento->id,
            'utente_id' => auth()->id(),
            'azione' => $azione,
            'dettagli' => $dettagli,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'dati_precedenti' => $documento->getOriginal(),
            'dati_nuovi' => $documento->getAttributes()
        ]);
    }
}

2. Observer per Audit Automatico

// app/Observers/DocumentoObserver.php
<?php

namespace App\Observers;

use App\Models\Documento;
use App\Models\AuditDocumento;

class DocumentoObserver
{
    public function created(Documento $documento)
    {
        AuditDocumento::logAzione($documento, 'created', [
            'messaggio' => 'Documento caricato'
        ]);
    }

    public function updated(Documento $documento)
    {
        $modifiche = [];
        foreach ($documento->getDirty() as $campo => $nuovoValore) {
            $modifiche[$campo] = [
                'da' => $documento->getOriginal($campo),
                'a' => $nuovoValore
            ];
        }

        AuditDocumento::logAzione($documento, 'updated', [
            'messaggio' => 'Documento modificato',
            'modifiche' => $modifiche
        ]);
    }

    public function deleted(Documento $documento)
    {
        AuditDocumento::logAzione($documento, 'deleted', [
            'messaggio' => 'Documento eliminato'
        ]);
    }
}

📋 CHECKLIST IMPLEMENTAZIONE

Database e Modelli

  • Migration tabelle documenti
  • Migration collegamenti documenti
  • Migration versioni documenti
  • Migration audit documenti
  • Modelli Eloquent completi
  • Observer per audit automatico

Servizi Core

  • DocumentoService per gestione base
  • Office365Service per integrazione cloud
  • GoogleDriveService per integrazione cloud
  • AuditService per tracciamento completo

Controller e API

  • DocumentoController per CRUD
  • API per upload tramite drag&drop
  • API per sincronizzazione cloud
  • API per ricerca avanzata

Frontend

  • Interfaccia upload documenti
  • Visualizzatore documenti integrato
  • Sistema ricerca e filtri
  • Dashboard audit e statistiche

Aggiornato: 2025-01-27
Versione: 1.0
Prossimo step: Implementazione migration e modelli base