# πŸ“ **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** ```sql -- 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** ```sql -- 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** ```sql 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** ```php // app/Models/Documento.php '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** ```php // app/Services/DocumentoService.php 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** ```php // app/Services/Office365Service.php 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** ```php // app/Services/GoogleDriveService.php 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** ```php // app/Models/AuditDocumento.php '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** ```php // app/Observers/DocumentoObserver.php '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