From d05865cb4da21a14af89b621acfe4ab18190cb88 Mon Sep 17 00:00:00 2001 From: michele Date: Sun, 6 Sep 2026 21:40:26 +0200 Subject: [PATCH] feat(posta,protocollo,ticket): studio imap mailboxes, stabili monitoring, multi-attachment resolution and ticket ui modernization --- .../Impostazioni/SchedaAmministratore.php | 113 ++++++ .../Pages/Posta/PostaOrdinariaWebmail.php | 163 +++++++-- app/Filament/Pages/Posta/PostaPecWebmail.php | 152 ++++++-- app/Filament/Pages/UnitaImmobiliarePage.php | 185 ++++++++-- .../Documenti/ArchiveFileResolver.php | 134 ++++++++ app/Services/Posta/ImapMailboxService.php | 169 +++++++++ ...amministratore-posta-operativita.blade.php | 206 ++++++++++- .../filament/pages/posta/webmail.blade.php | 33 +- .../pages/supporto/ticket-gestione.blade.php | 324 +++++++++++++----- .../pages/unita-immobiliare.blade.php | 36 +- skill-netgescon/control-tower/CURRENT-205.md | 60 ++-- .../PostaImapWebmailAndProtocolloTest.php | 41 +++ 12 files changed, 1382 insertions(+), 234 deletions(-) create mode 100644 app/Services/Documenti/ArchiveFileResolver.php diff --git a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php index 9934eac..29b9d9c 100755 --- a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php +++ b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php @@ -2094,4 +2094,117 @@ private function safeAmministratoreImpostazioni(Amministratore $amministratore): return is_array($settings) ? $settings : []; } + + public function getStabiliMailboxStatusProperty(): array + { + $user = Auth::user(); + $stabili = $user instanceof User ? StabileContext::accessibleStabili($user) : Stabile::all(); + $res = []; + + foreach ($stabili as $s) { + $cfg = (array) ($s->configurazione_avanzata ?? []); + $posta = (array) ($cfg['posta'] ?? []); + $caselle = (array) ($posta['caselle'] ?? []); + + $emailConfigured = false; + $emailInfo = ''; + $pecConfigured = false; + $pecInfo = ''; + + foreach ($caselle as $c) { + if (! is_array($c) || empty($c['enabled'])) { + continue; + } + $isPec = ($c['tipo'] ?? '') === 'pec'; + $hasConn = ! empty($c['host']) && ! empty($c['username']); + if ($isPec) { + if ($hasConn) { + $pecConfigured = true; + $pecInfo = ($c['email'] ?? $c['username'] ?? '') . ' (' . ($c['host'] ?? '') . ')'; + } + } else { + if ($hasConn) { + $emailConfigured = true; + $emailInfo = ($c['email'] ?? $c['username'] ?? '') . ' (' . ($c['host'] ?? '') . ')'; + } + } + } + + $msgCount = CommunicationMessage::where('stabile_id', $s->id)->count(); + + $res[] = [ + 'id' => (int) $s->id, + 'codice_stabile' => (string) $s->codice_stabile, + 'denominazione' => (string) $s->denominazione, + 'comune' => (string) $s->comune, + 'codice_fiscale' => (string) $s->codice_fiscale, + 'email_configured' => $emailConfigured, + 'email_info' => $emailInfo, + 'pec_configured' => $pecConfigured, + 'pec_info' => $pecInfo, + 'msg_count' => $msgCount, + 'config_url' => url('/admin-filament/condomini/stabile?stabile_id=' . $s->id . '&tab=posta-ufficiale'), + 'webmail_email_url' => url('/admin-filament/comunicazioni/posta-ordinaria?stabile_id=' . $s->id), + 'webmail_pec_url' => url('/admin-filament/comunicazioni/posta-pec?stabile_id=' . $s->id), + ]; + } + + return $res; + } + + public function testStudioMailbox(int $index): void + { + $caselle = (array) data_get($this->data, 'impostazioni.posta.caselle', []); + $box = $caselle[$index] ?? null; + + if (! is_array($box)) { + Notification::make()->title('Configurazione casella non trovata')->danger()->send(); + return; + } + + $service = app(\App\Services\Posta\ImapMailboxService::class); + $res = $service->testMailbox($box); + + if ($res['success']) { + Notification::make() + ->title('Connessione IMAP Studio Riuscita!') + ->body($res['message']) + ->success() + ->send(); + } else { + Notification::make() + ->title('Test Connessione Fallito') + ->body($res['message']) + ->danger() + ->send(); + } + } + + public function syncStudioMailbox(int $index): void + { + $caselle = (array) data_get($this->data, 'impostazioni.posta.caselle', []); + $box = $caselle[$index] ?? null; + + if (! is_array($box)) { + Notification::make()->title('Configurazione casella non trovata')->danger()->send(); + return; + } + + $service = app(\App\Services\Posta\ImapMailboxService::class); + $res = $service->fetchStudioMailbox($this->amministratore, $box, 30); + + if (($res['imported'] ?? 0) > 0) { + Notification::make() + ->title('Sincronizzazione completata') + ->body("Importati {$res['imported']} nuovi messaggi (.EML)") + ->success() + ->send(); + } else { + Notification::make() + ->title('Nessun nuovo messaggio') + ->body($res['message'] ?? 'Nessun messaggio trovato da scaricare.') + ->info() + ->send(); + } + } } diff --git a/app/Filament/Pages/Posta/PostaOrdinariaWebmail.php b/app/Filament/Pages/Posta/PostaOrdinariaWebmail.php index 3631966..dc6602b 100644 --- a/app/Filament/Pages/Posta/PostaOrdinariaWebmail.php +++ b/app/Filament/Pages/Posta/PostaOrdinariaWebmail.php @@ -36,7 +36,7 @@ class PostaOrdinariaWebmail extends Page public string $tipoCasella = 'email'; // 'email' o 'pec' - public ?int $selectedStabileId = null; + public string|int|null $selectedStabileId = null; public string $currentFolder = 'inbox'; // 'inbox', 'starred', 'sent', 'drafts', 'trash', 'attachments' @@ -68,9 +68,11 @@ public function mount(): void { $user = Auth::user(); if ($user instanceof User) { - $reqStabile = request()->integer('stabile_id'); - if ($reqStabile > 0) { - $this->selectedStabileId = $reqStabile; + $reqStabile = request()->query('stabile_id'); + if ($reqStabile === 'studio') { + $this->selectedStabileId = 'studio'; + } elseif (is_numeric($reqStabile) && (int) $reqStabile > 0) { + $this->selectedStabileId = (int) $reqStabile; } else { $this->selectedStabileId = StabileContext::resolveActiveStabileId($user); } @@ -84,9 +86,66 @@ public function getStabiliOptionsProperty(): array return []; } - return StabileContext::accessibleStabili($user) + $options = [ + 'studio' => '🏢 Posta Generale Studio / Amministrazione', + ]; + + $stabili = StabileContext::accessibleStabili($user) ->mapWithKeys(fn(Stabile $s) => [(int) $s->id => trim((string) ($s->codice_stabile . ' - ' . $s->denominazione))]) ->all(); + + return $options + $stabili; + } + + public function getConfigMailboxUrlProperty(): string + { + if ($this->selectedStabileId === 'studio') { + return url('/admin-filament/impostazioni/scheda-amministratore?tab=posta-operativita'); + } + + if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + return url('/admin-filament/condomini/stabile?stabile_id=' . $this->selectedStabileId . '&tab=posta-ufficiale'); + } + + return url('/admin-filament/condomini/stabile?tab=posta-ufficiale'); + } + + public function getIsMailboxConfiguredProperty(): bool + { + $user = Auth::user(); + if (! $user instanceof User) { + return false; + } + + if ($this->selectedStabileId === 'studio') { + $amministratore = $user->amministratore; + if ($amministratore) { + $config = (array) ($amministratore->impostazioni ?? []); + $caselle = (array) ($config['posta']['caselle'] ?? []); + foreach ($caselle as $c) { + if (! empty($c['enabled']) && ! empty($c['host']) && ($this->tipoCasella === 'pec' ? ($c['tipo'] ?? '') === 'pec' : ($c['tipo'] ?? '') !== 'pec')) { + return true; + } + } + } + return false; + } + + if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + $stabile = Stabile::find($this->selectedStabileId); + if ($stabile) { + $config = (array) ($stabile->configurazione_avanzata ?? []); + $caselle = (array) ($config['posta']['caselle'] ?? []); + foreach ($caselle as $c) { + if (! empty($c['enabled']) && ! empty($c['host']) && ($this->tipoCasella === 'pec' ? ($c['tipo'] ?? '') === 'pec' : ($c['tipo'] ?? '') !== 'pec')) { + return true; + } + } + } + return false; + } + + return true; } public function selectFolder(string $folder): void @@ -174,31 +233,56 @@ public function syncNow(): void } $service = app(ImapMailboxService::class); - $stabili = $this->selectedStabileId - ? Stabile::where('id', $this->selectedStabileId)->get() - : StabileContext::accessibleStabili($user); - $totImported = 0; - foreach ($stabili as $stabile) { - $config = (array) ($stabile->configurazione_avanzata ?? []); - $posta = (array) ($config['posta'] ?? []); - $caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array')); - foreach ($caselle as $mailbox) { - if (empty($mailbox['enabled'])) { - continue; - } - $mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap')); - if ($this->tipoCasella === 'pec' && $mTipo !== 'pec') { - continue; - } - if ($this->tipoCasella === 'email' && $mTipo === 'pec') { - continue; + if ($this->selectedStabileId === 'studio') { + $amministratore = $user->amministratore; + if ($amministratore) { + $config = (array) ($amministratore->impostazioni ?? []); + $caselle = array_values(array_filter((array) ($config['posta']['caselle'] ?? []), 'is_array')); + foreach ($caselle as $mailbox) { + if (empty($mailbox['enabled'])) { + continue; + } + $mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap')); + if ($this->tipoCasella === 'pec' && $mTipo !== 'pec') { + continue; + } + if ($this->tipoCasella === 'email' && $mTipo === 'pec') { + continue; + } + if (! empty($mailbox['host']) && ! empty($mailbox['username'])) { + $res = $service->fetchStudioMailbox($amministratore, $mailbox, 20); + $totImported += (int) ($res['imported'] ?? 0); + } } + } + } else { + $stabili = (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) + ? Stabile::where('id', (int) $this->selectedStabileId)->get() + : StabileContext::accessibleStabili($user); - if (! empty($mailbox['host']) && ! empty($mailbox['username'])) { - $res = $service->fetchMailbox($stabile, $mailbox, 20); - $totImported += (int) ($res['imported'] ?? 0); + foreach ($stabili as $stabile) { + $config = (array) ($stabile->configurazione_avanzata ?? []); + $posta = (array) ($config['posta'] ?? []); + $caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array')); + + foreach ($caselle as $mailbox) { + if (empty($mailbox['enabled'])) { + continue; + } + $mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap')); + if ($this->tipoCasella === 'pec' && $mTipo !== 'pec') { + continue; + } + if ($this->tipoCasella === 'email' && $mTipo === 'pec') { + continue; + } + + if (! empty($mailbox['host']) && ! empty($mailbox['username'])) { + $res = $service->fetchMailbox($stabile, $mailbox, 20); + $totImported += (int) ($res['imported'] ?? 0); + } } } } @@ -283,8 +367,7 @@ public function getMessagesProperty() ? StabileContext::accessibleStabili($user)->pluck('id')->toArray() : []; - $query = CommunicationMessage::with(['stabile', 'ticket']) - ->whereIn('stabile_id', $allowedStabileIds); + $query = CommunicationMessage::with(['stabile', 'ticket']); if ($this->tipoCasella === 'pec') { $query->where('channel', 'pec'); @@ -292,8 +375,15 @@ public function getMessagesProperty() $query->whereIn('channel', ['email', 'gmail']); } - if ($this->selectedStabileId) { - $query->where('stabile_id', $this->selectedStabileId); + if ($this->selectedStabileId === 'studio') { + $query->whereNull('stabile_id'); + } elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + $query->where('stabile_id', (int) $this->selectedStabileId); + } else { + $query->where(function ($q) use ($allowedStabileIds) { + $q->whereIn('stabile_id', $allowedStabileIds) + ->orWhereNull('stabile_id'); + }); } if ($this->currentFolder === 'sent') { @@ -335,15 +425,22 @@ public function getFolderCountsProperty(): array ? StabileContext::accessibleStabili($user)->pluck('id')->toArray() : []; - $base = CommunicationMessage::whereIn('stabile_id', $allowedStabileIds); + $base = CommunicationMessage::query(); if ($this->tipoCasella === 'pec') { $base->where('channel', 'pec'); } else { $base->whereIn('channel', ['email', 'gmail']); } - if ($this->selectedStabileId) { - $base->where('stabile_id', $this->selectedStabileId); + if ($this->selectedStabileId === 'studio') { + $base->whereNull('stabile_id'); + } elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + $base->where('stabile_id', (int) $this->selectedStabileId); + } else { + $base->where(function ($q) use ($allowedStabileIds) { + $q->whereIn('stabile_id', $allowedStabileIds) + ->orWhereNull('stabile_id'); + }); } return [ diff --git a/app/Filament/Pages/Posta/PostaPecWebmail.php b/app/Filament/Pages/Posta/PostaPecWebmail.php index 3066d6d..4270f90 100644 --- a/app/Filament/Pages/Posta/PostaPecWebmail.php +++ b/app/Filament/Pages/Posta/PostaPecWebmail.php @@ -38,7 +38,7 @@ class PostaPecWebmail extends Page public string $tipoCasella = 'pec'; - public ?int $selectedStabileId = null; + public string|int|null $selectedStabileId = null; public string $currentFolder = 'inbox'; // 'inbox', 'starred', 'sent', 'drafts', 'trash', 'attachments' @@ -70,9 +70,11 @@ public function mount(): void { $user = Auth::user(); if ($user instanceof User) { - $reqStabile = request()->integer('stabile_id'); - if ($reqStabile > 0) { - $this->selectedStabileId = $reqStabile; + $reqStabile = request()->query('stabile_id'); + if ($reqStabile === 'studio') { + $this->selectedStabileId = 'studio'; + } elseif (is_numeric($reqStabile) && (int) $reqStabile > 0) { + $this->selectedStabileId = (int) $reqStabile; } else { $this->selectedStabileId = StabileContext::resolveActiveStabileId($user); } @@ -86,9 +88,66 @@ public function getStabiliOptionsProperty(): array return []; } - return StabileContext::accessibleStabili($user) + $options = [ + 'studio' => '🏢 PEC Generale Studio / Amministrazione', + ]; + + $stabili = StabileContext::accessibleStabili($user) ->mapWithKeys(fn(Stabile $s) => [(int) $s->id => trim((string) ($s->codice_stabile . ' - ' . $s->denominazione))]) ->all(); + + return $options + $stabili; + } + + public function getConfigMailboxUrlProperty(): string + { + if ($this->selectedStabileId === 'studio') { + return url('/admin-filament/impostazioni/scheda-amministratore?tab=posta-operativita'); + } + + if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + return url('/admin-filament/condomini/stabile?stabile_id=' . $this->selectedStabileId . '&tab=posta-ufficiale'); + } + + return url('/admin-filament/condomini/stabile?tab=posta-ufficiale'); + } + + public function getIsMailboxConfiguredProperty(): bool + { + $user = Auth::user(); + if (! $user instanceof User) { + return false; + } + + if ($this->selectedStabileId === 'studio') { + $amministratore = $user->amministratore; + if ($amministratore) { + $config = (array) ($amministratore->impostazioni ?? []); + $caselle = (array) ($config['posta']['caselle'] ?? []); + foreach ($caselle as $c) { + if (! empty($c['enabled']) && ! empty($c['host']) && ($c['tipo'] ?? '') === 'pec') { + return true; + } + } + } + return false; + } + + if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + $stabile = Stabile::find($this->selectedStabileId); + if ($stabile) { + $config = (array) ($stabile->configurazione_avanzata ?? []); + $caselle = (array) ($config['posta']['caselle'] ?? []); + foreach ($caselle as $c) { + if (! empty($c['enabled']) && ! empty($c['host']) && ($c['tipo'] ?? '') === 'pec') { + return true; + } + } + } + return false; + } + + return true; } public function selectFolder(string $folder): void @@ -176,28 +235,50 @@ public function syncNow(): void } $service = app(ImapMailboxService::class); - $stabili = $this->selectedStabileId - ? Stabile::where('id', $this->selectedStabileId)->get() - : StabileContext::accessibleStabili($user); - $totImported = 0; - foreach ($stabili as $stabile) { - $config = (array) ($stabile->configurazione_avanzata ?? []); - $posta = (array) ($config['posta'] ?? []); - $caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array')); - foreach ($caselle as $mailbox) { - if (empty($mailbox['enabled'])) { - continue; - } - $mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap')); - if ($mTipo !== 'pec') { - continue; + if ($this->selectedStabileId === 'studio') { + $amministratore = $user->amministratore; + if ($amministratore) { + $config = (array) ($amministratore->impostazioni ?? []); + $caselle = array_values(array_filter((array) ($config['posta']['caselle'] ?? []), 'is_array')); + foreach ($caselle as $mailbox) { + if (empty($mailbox['enabled'])) { + continue; + } + $mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap')); + if ($mTipo !== 'pec') { + continue; + } + if (! empty($mailbox['host']) && ! empty($mailbox['username'])) { + $res = $service->fetchStudioMailbox($amministratore, $mailbox, 20); + $totImported += (int) ($res['imported'] ?? 0); + } } + } + } else { + $stabili = (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) + ? Stabile::where('id', (int) $this->selectedStabileId)->get() + : StabileContext::accessibleStabili($user); - if (! empty($mailbox['host']) && ! empty($mailbox['username'])) { - $res = $service->fetchMailbox($stabile, $mailbox, 20); - $totImported += (int) ($res['imported'] ?? 0); + foreach ($stabili as $stabile) { + $config = (array) ($stabile->configurazione_avanzata ?? []); + $posta = (array) ($config['posta'] ?? []); + $caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array')); + + foreach ($caselle as $mailbox) { + if (empty($mailbox['enabled'])) { + continue; + } + $mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap')); + if ($mTipo !== 'pec') { + continue; + } + + if (! empty($mailbox['host']) && ! empty($mailbox['username'])) { + $res = $service->fetchMailbox($stabile, $mailbox, 20); + $totImported += (int) ($res['imported'] ?? 0); + } } } } @@ -283,11 +364,17 @@ public function getMessagesProperty() : []; $query = CommunicationMessage::with(['stabile', 'ticket']) - ->whereIn('stabile_id', $allowedStabileIds) ->where('channel', 'pec'); - if ($this->selectedStabileId) { - $query->where('stabile_id', $this->selectedStabileId); + if ($this->selectedStabileId === 'studio') { + $query->whereNull('stabile_id'); + } elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + $query->where('stabile_id', (int) $this->selectedStabileId); + } else { + $query->where(function ($q) use ($allowedStabileIds) { + $q->whereIn('stabile_id', $allowedStabileIds) + ->orWhereNull('stabile_id'); + }); } if ($this->currentFolder === 'sent') { @@ -329,10 +416,17 @@ public function getFolderCountsProperty(): array ? StabileContext::accessibleStabili($user)->pluck('id')->toArray() : []; - $base = CommunicationMessage::whereIn('stabile_id', $allowedStabileIds)->where('channel', 'pec'); + $base = CommunicationMessage::query()->where('channel', 'pec'); - if ($this->selectedStabileId) { - $base->where('stabile_id', $this->selectedStabileId); + if ($this->selectedStabileId === 'studio') { + $base->whereNull('stabile_id'); + } elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) { + $base->where('stabile_id', (int) $this->selectedStabileId); + } else { + $base->where(function ($q) use ($allowedStabileIds) { + $q->whereIn('stabile_id', $allowedStabileIds) + ->orWhereNull('stabile_id'); + }); } return [ diff --git a/app/Filament/Pages/UnitaImmobiliarePage.php b/app/Filament/Pages/UnitaImmobiliarePage.php index 6aa3e78..485f132 100755 --- a/app/Filament/Pages/UnitaImmobiliarePage.php +++ b/app/Filament/Pages/UnitaImmobiliarePage.php @@ -1186,6 +1186,7 @@ public function getProtocolloComunicazioniProperty(): array $codStabile = (string) $this->unita->stabile->codice_stabile; $scala = trim((string) $this->unita->scala); $interno = trim((string) $this->unita->interno); + $condIds = $this->resolveLegacyCondIdsForUnita(); $results = []; @@ -1193,16 +1194,19 @@ public function getProtocolloComunicazioniProperty(): array if (DbSchema::connection('gescon_import')->hasTable('protoc_ec')) { $ecQuery = DB::connection('gescon_import')->table('protoc_ec') ->where('cod_stabile', $codStabile) - ->where(function ($q) use ($scala, $interno) { + ->where(function ($q) use ($scala, $interno, $condIds) { $q->where(function ($sub) use ($scala, $interno) { $sub->where('scala', $scala)->where('interno', $interno); }); + if (! empty($condIds)) { + $q->orWhereIn('id_condomino', $condIds); + } }) ->orderByDesc('id') ->get(); foreach ($ecQuery as $r) { - $dtRaw = $r->data_invio ?? null; + $dtRaw = $r->data_invio ?? ($r->data ?? null); $ts = 0; $dtFmt = '—'; if ($dtRaw) { @@ -1215,24 +1219,34 @@ public function getProtocolloComunicazioniProperty(): array } } - $pdfName = trim((string) ($r->nome_pdf ?? '')); - $pdfPath = $pdfName !== '' ? "/mnt/gescon-archives/gescon/{$codStabile}/E_C/{$pdfName}" : null; - $pdfExists = $pdfPath && file_exists($pdfPath); + $candidateNames = array_values(array_unique(array_filter([ + trim((string) ($r->nome_pdf ?? '')), + trim((string) ($r->alleg_4 ?? '')), + trim((string) ($r->lettera_tipo_caricata ?? '')), + ]))); + + $allegati = []; + foreach ($candidateNames as $fn) { + $allegati[] = \App\Services\Documenti\ArchiveFileResolver::resolve($codStabile, $fn, 'E_C'); + } + + $firstAtt = $allegati[0] ?? null; $results[] = [ 'id' => 'ec_' . $r->id, - 'protocollo' => $r->protocollo ?: ('EC #' . $r->id_corrisp), + 'protocollo' => $r->protocollo ?: ('EC #' . ($r->id_corrisp ?? $r->id)), 'tipo' => 'Estratto Conto', 'canale' => $r->tipo_documento ?: 'E-Mail', 'data_invio' => $dtFmt, - 'destinatario' => $r->destinatario ?: $r->nome, - 'ruolo' => $r->c_i === 'I' ? 'Inquilino' : 'Condomino', + 'destinatario' => $r->destinatario ?: ($r->nome ?? '—'), + 'ruolo' => ($r->c_i ?? '') === 'I' ? 'Inquilino' : 'Condomino', 'oggetto' => $r->oggetto ?: 'Invio Estratto Conto', 'importo' => (float) ($r->totale ?? 0), - 'nome_file' => $pdfName, - 'file_path' => $pdfPath, - 'file_exists' => $pdfExists, - 'note' => $r->note, + 'allegati' => $allegati, + 'nome_file' => $firstAtt['filename'] ?? null, + 'file_path' => $firstAtt['path'] ?? null, + 'file_exists' => $firstAtt['exists'] ?? false, + 'note' => $r->note ?? null, 'timestamp_sort' => $ts, ]; } @@ -1240,7 +1254,6 @@ public function getProtocolloComunicazioniProperty(): array // 2. Dati da corrisp_inviata (Lettere, circolari) if (DbSchema::connection('gescon_import')->hasTable('corrisp_inviata')) { - $condIds = $this->resolveLegacyCondIdsForUnita(); $corrQuery = DB::connection('gescon_import')->table('corrisp_inviata') ->where('cod_stabile', $codStabile) ->where(function ($q) use ($condIds) { @@ -1253,7 +1266,7 @@ public function getProtocolloComunicazioniProperty(): array ->get(); foreach ($corrQuery as $r) { - $dtRaw = $r->data_invio ?? null; + $dtRaw = $r->data_invio ?? ($r->data ?? null); $ts = 0; $dtFmt = '—'; if ($dtRaw) { @@ -1266,30 +1279,112 @@ public function getProtocolloComunicazioniProperty(): array } } - $docName = trim((string) ($r->lettera_tipo_caricata ?? '')); - $docPath = $docName !== '' ? "/mnt/gescon-archives/gescon/{$codStabile}/{$docName}" : null; - $docExists = $docPath && file_exists($docPath); + $candidateNames = array_values(array_unique(array_filter([ + trim((string) ($r->lettera_tipo_caricata ?? '')), + trim((string) ($r->nome_alleg_1 ?? '')), + trim((string) ($r->nome_alleg_2 ?? '')), + trim((string) ($r->nome_alleg_3 ?? '')), + trim((string) ($r->nome_alleg_4 ?? '')), + ]))); + + $allegati = []; + foreach ($candidateNames as $fn) { + $allegati[] = \App\Services\Documenti\ArchiveFileResolver::resolve($codStabile, $fn, 'allegati'); + } + + $firstAtt = $allegati[0] ?? null; $results[] = [ 'id' => 'corr_' . $r->id, - 'protocollo' => $r->protocollo ?: ('PROT #' . $r->id_corrisp), + 'protocollo' => $r->protocollo ?: ('PROT #' . ($r->id_corrisp ?? $r->id)), 'tipo' => 'Lettera / Circolare', 'canale' => $r->tipo_documento ?: 'Posta', 'data_invio' => $dtFmt, - 'destinatario' => $r->destinatario ?: 'Condomini', + 'destinatario' => $r->destinatario ?: ($r->nome ?? 'Condomini'), 'ruolo' => 'Condomino', 'oggetto' => $r->oggetto ?: 'Comunicazione Amministrazione', 'importo' => null, - 'nome_file' => $docName, - 'file_path' => $docPath, - 'file_exists' => $docExists, - 'note' => $r->note, + 'allegati' => $allegati, + 'nome_file' => $firstAtt['filename'] ?? null, + 'file_path' => $firstAtt['path'] ?? null, + 'file_exists' => $firstAtt['exists'] ?? false, + 'note' => $r->note ?? null, 'timestamp_sort' => $ts, ]; } } - // 3. Dati da CommunicationMessage (Posta IMAP, PEC, EML) + // 3. Dati da posta_dett (Raccomandate, visure, postalizzazione) + if (DbSchema::connection('gescon_import')->hasTable('posta_dett')) { + $postaQuery = DB::connection('gescon_import')->table('posta_dett') + ->where(function ($q) use ($scala, $interno, $condIds) { + $hasCond = false; + if ($scala !== '' && $interno !== '') { + $q->where(function ($sub) use ($scala, $interno) { + $sub->where('scala', $scala)->where('interno', $interno); + }); + $hasCond = true; + } + if (! empty($condIds)) { + if ($hasCond) { + $q->orWhereIn('id_cond', $condIds); + } else { + $q->whereIn('id_cond', $condIds); + } + } + }) + ->orderByDesc('id') + ->get(); + + foreach ($postaQuery as $r) { + $dtRaw = $r->ora_invio_richiesta ?? ($r->data ?? null); + $ts = 0; + $dtFmt = '—'; + if ($dtRaw) { + try { + $c = Carbon::parse((string) $dtRaw); + $ts = $c->timestamp; + $dtFmt = $c->format('d/m/Y'); + } catch (\Throwable $e) { + $dtFmt = (string) $dtRaw; + } + } + + $candidateNames = array_values(array_unique(array_filter([ + trim((string) ($r->nome_alleg_1 ?? '')), + trim((string) ($r->nome_alleg_4 ?? '')), + trim((string) ($r->documento ?? '')), + trim((string) ($r->risposta ?? '')), + ]))); + + $allegati = []; + foreach ($candidateNames as $fn) { + $allegati[] = \App\Services\Documenti\ArchiveFileResolver::resolve($codStabile, $fn); + } + + $firstAtt = $allegati[0] ?? null; + + $results[] = [ + 'id' => 'posta_dett_' . $r->id, + 'protocollo' => $r->num_raccomandata ? ('RAC #' . $r->num_raccomandata) : ('POSTA #' . ($r->id_richiesta ?? $r->id)), + 'tipo' => $r->tipo_richiesta ?: 'Spedizione Postale / Visura', + 'canale' => 'Posta / Raccomandata', + 'data_invio' => $dtFmt, + 'destinatario' => $r->nome ?: ($r->destinatario ?? '—'), + 'ruolo' => ($r->c_i ?? '') === 'I' ? 'Inquilino' : 'Condomino', + 'oggetto' => $r->stato_richiesta ? ('Stato invio: ' . $r->stato_richiesta) : 'Invio Raccomandata / Documento', + 'importo' => (float) ($r->costo ?? ($r->importo_ec ?? 0)), + 'allegati' => $allegati, + 'nome_file' => $firstAtt['filename'] ?? null, + 'file_path' => $firstAtt['path'] ?? null, + 'file_exists' => $firstAtt['exists'] ?? false, + 'note' => $r->stato_richiesta ?? null, + 'timestamp_sort' => $ts, + ]; + } + } + + // 4. Dati da CommunicationMessage (Posta IMAP, PEC, EML) if (DbSchema::hasTable('communication_messages')) { $unitEmails = []; $recapiti = $this->getRecapitiMulticanaleTableProperty(); @@ -1317,8 +1412,39 @@ public function getProtocolloComunicazioniProperty(): array $dt = $cm->received_at ? Carbon::parse($cm->received_at) : null; $dtFmt = $dt ? $dt->format('d/m/Y H:i') : '—'; $ts = $dt ? $dt->timestamp : 0; - $attachments = is_array($cm->attachments) ? $cm->attachments : []; - $firstAtt = $attachments[0] ?? null; + $rawAttachments = is_array($cm->attachments) ? $cm->attachments : []; + + $allegati = []; + foreach ($rawAttachments as $att) { + $fn = (string) ($att['filename'] ?? 'allegato'); + $fp = (string) ($att['path'] ?? ''); + $exists = $fp !== '' && file_exists($fp); + $allegati[] = [ + 'filename' => $fn, + 'path' => $fp, + 'exists' => $exists, + 'size' => (int) ($att['size'] ?? 0), + 'mime' => (string) ($att['content_type'] ?? 'application/octet-stream'), + 'is_pdf' => str_ends_with(strtolower($fn), '.pdf'), + 'is_image' => (bool) preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $fn), + ]; + } + + if (empty($allegati) && ! empty($meta['eml_path'])) { + $emlPath = (string) $meta['eml_path']; + $emlName = basename($emlPath); + $allegati[] = [ + 'filename' => $emlName, + 'path' => $emlPath, + 'exists' => file_exists($emlPath), + 'size' => file_exists($emlPath) ? (int) filesize($emlPath) : 0, + 'mime' => 'message/rfc822', + 'is_pdf' => false, + 'is_image' => false, + ]; + } + + $firstAtt = $allegati[0] ?? null; $results[] = [ 'id' => 'comm_' . $cm->id, @@ -1330,9 +1456,10 @@ public function getProtocolloComunicazioniProperty(): array 'ruolo' => $cm->channel === 'pec' ? 'PEC' : 'Email', 'oggetto' => $meta['subject'] ?? \Illuminate\Support\Str::limit($cm->message_text, 60), 'importo' => null, - 'nome_file' => $firstAtt['filename'] ?? (! empty($meta['eml_path']) ? basename($meta['eml_path']) : null), - 'file_path' => $firstAtt['path'] ?? ($meta['eml_path'] ?? null), - 'file_exists' => ($firstAtt['path'] ?? null) ? file_exists($firstAtt['path']) : (! empty($meta['eml_path']) && file_exists($meta['eml_path'])), + 'allegati' => $allegati, + 'nome_file' => $firstAtt['filename'] ?? null, + 'file_path' => $firstAtt['path'] ?? null, + 'file_exists' => $firstAtt['exists'] ?? false, 'note' => $cm->message_text ? \Illuminate\Support\Str::limit($cm->message_text, 120) : null, 'timestamp_sort' => $ts, ]; diff --git a/app/Services/Documenti/ArchiveFileResolver.php b/app/Services/Documenti/ArchiveFileResolver.php new file mode 100644 index 0000000..840742a --- /dev/null +++ b/app/Services/Documenti/ArchiveFileResolver.php @@ -0,0 +1,134 @@ + '', + 'path' => null, + 'exists' => false, + 'size' => 0, + 'mime' => 'application/octet-stream', + 'is_pdf' => false, + 'is_image' => false, + ]; + } + + $cod = str_pad((string) $codStabile, 4, '0', STR_PAD_LEFT); + $candidateSubdirs = [ + $preferredSubdir, + 'E_C', 'e_c', + 'INC_EC', 'inc_ec', + 'allegati', 'Allegati', + 'WA', 'wa', + 'XML', 'xml', + 'posta_ordinaria', 'posta_pec', 'posta', + 'documenti', 'Documenti', + '', + ]; + $candidateSubdirs = array_values(array_unique(array_filter($candidateSubdirs, fn($v) => $v !== null))); + + $baseRoots = [ + "/mnt/gescon-archives/gescon/{$cod}", + "/mnt/gescon-archives/gescon/" . ltrim((string) $codStabile, '0'), + storage_path("app/private/amministratori/ADMX3PD9/stabili/{$cod}"), + storage_path("app/private/amministratori/ADMX3PD9/stabili/" . ltrim((string) $codStabile, '0')), + storage_path("app/public/stabili/{$cod}"), + storage_path("app/public/stabili/" . ltrim((string) $codStabile, '0')), + ]; + + $foundPath = null; + + // 1. Cerca percorsi esatti + foreach ($baseRoots as $root) { + if (! is_dir($root)) { + continue; + } + foreach ($candidateSubdirs as $sub) { + $dir = $sub !== '' ? "{$root}/{$sub}" : $root; + $p = "{$dir}/{$filename}"; + if (file_exists($p)) { + $foundPath = $p; + break 2; + } + } + } + + // 2. Se non trovato e il file ha un'estensione, cerca case-insensitive + if (! $foundPath) { + $lowerName = strtolower($filename); + foreach ($baseRoots as $root) { + if (! is_dir($root)) { + continue; + } + foreach ($candidateSubdirs as $sub) { + $dir = $sub !== '' ? "{$root}/{$sub}" : $root; + if (is_dir($dir)) { + $files = @scandir($dir); + if (is_array($files)) { + foreach ($files as $f) { + if (strtolower($f) === $lowerName) { + $foundPath = "{$dir}/{$f}"; + break 3; + } + } + } + } + } + } + } + + $exists = $foundPath !== null && file_exists($foundPath); + $size = $exists ? (int) @filesize($foundPath) : 0; + $mime = 'application/octet-stream'; + $isPdf = str_ends_with(strtolower($filename), '.pdf'); + $isImg = (bool) preg_match('/\.(jpg|jpeg|png|webp|gif|bmp)$/i', $filename); + + if ($exists) { + $finfo = @finfo_open(FILEINFO_MIME_TYPE); + if ($finfo) { + $detected = @finfo_file($finfo, $foundPath); + if ($detected) { + $mime = $detected; + } + @finfo_close($finfo); + } + } + + if ($isPdf) { + $mime = 'application/pdf'; + } + + return [ + 'filename' => $filename, + 'path' => $foundPath, + 'exists' => $exists, + 'size' => $size, + 'mime' => $mime, + 'is_pdf' => $isPdf, + 'is_image' => $isImg, + ]; + } +} diff --git a/app/Services/Posta/ImapMailboxService.php b/app/Services/Posta/ImapMailboxService.php index eae699a..ddba08a 100644 --- a/app/Services/Posta/ImapMailboxService.php +++ b/app/Services/Posta/ImapMailboxService.php @@ -107,6 +107,175 @@ public function fetchMailbox(Stabile $stabile, array $mailbox, int $maxMessages return $stats; } + /** + * Scarica e memorizza le email/PEC dello studio amministratore in formato .EML + * + * @param \App\Models\Amministratore $amministratore + * @param array $mailbox + * @param int $maxMessages + * @return array{imported: int, skipped: int, errors: int, messages: array} + */ + public function fetchStudioMailbox(\App\Models\Amministratore $amministratore, array $mailbox, int $maxMessages = 50): array + { + $host = trim((string) ($mailbox['host'] ?? '')); + $port = (int) ($mailbox['port'] ?? 993); + $username = trim((string) ($mailbox['username'] ?? ($mailbox['email'] ?? ''))); + $password = (string) ($mailbox['password'] ?? ''); + $encryption = trim((string) ($mailbox['encryption'] ?? 'ssl')) ?: 'ssl'; + $folder = trim((string) ($mailbox['folder'] ?? 'INBOX')) ?: 'INBOX'; + $isPec = strtolower(trim((string) ($mailbox['tipo'] ?? ''))) === 'pec'; + + if ($host === '' || $username === '') { + return [ + 'imported' => 0, + 'skipped' => 0, + 'errors' => 1, + 'message' => 'Parametri IMAP studio incompleti', + 'messages' => [], + ]; + } + + $stats = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'messages' => []]; + + try { + $this->imapClient->connect($host, $port, $username, $password, $encryption); + $this->imapClient->selectFolder($folder); + $msgIds = $this->imapClient->search('ALL'); + + $recentIds = array_slice(array_reverse($msgIds), 0, $maxMessages); + + foreach ($recentIds as $msgId) { + try { + $rawEml = $this->imapClient->fetchRawEml($msgId); + if (trim($rawEml) === '') { + continue; + } + + $res = $this->ingestStudioEmlString($rawEml, $amministratore, $mailbox, $isPec); + if ($res['status'] === 'imported') { + $stats['imported']++; + $stats['messages'][] = $res; + } elseif ($res['status'] === 'duplicate') { + $stats['skipped']++; + } else { + $stats['errors']++; + } + } catch (\Throwable $e) { + $stats['errors']++; + } + } + + $this->imapClient->disconnect(); + } catch (\Throwable $e) { + $stats['errors']++; + $stats['error_message'] = $e->getMessage(); + } + + return $stats; + } + + /** + * Ingesta un messaggio EML dello studio amministratore + */ + public function ingestStudioEmlString( + string $rawEml, + \App\Models\Amministratore $amministratore, + array $mailboxConfig = [], + bool $forcePec = false + ): array { + $parsed = $this->emlParser->parse($rawEml); + $messageId = $parsed['message_id'] ?: ('studio_' . md5($rawEml)); + + $existing = CommunicationMessage::whereNull('stabile_id') + ->where(function ($q) use ($messageId) { + $q->where('external_message_id', $messageId) + ->orWhere('metadata->message_id', $messageId); + }) + ->first(); + + if ($existing) { + return ['status' => 'duplicate', 'id' => $existing->id]; + } + + $channel = ($forcePec || $parsed['is_pec']) ? 'pec' : 'email'; + $year = $parsed['date'] ? $parsed['date']->format('Y') : date('Y'); + + $postaSubdir = $channel === 'pec' ? 'posta_pec' : 'posta_ordinaria'; + $storageDir = $this->pathService->amministratoreAbsolutePath($amministratore, "studio/{$postaSubdir}/{$year}"); + if (! is_dir($storageDir)) { + @mkdir($storageDir, 0755, true); + } + + $safeMsgId = preg_replace('/[^a-zA-Z0-9_-]/', '_', $messageId); + $emlFilename = "{$safeMsgId}.eml"; + $emlFullPath = "{$storageDir}/{$emlFilename}"; + @file_put_contents($emlFullPath, $rawEml); + + $savedAttachments = []; + if (! empty($parsed['attachments'])) { + $attachmentsDir = "{$storageDir}/allegati/{$safeMsgId}"; + if (! is_dir($attachmentsDir)) { + @mkdir($attachmentsDir, 0755, true); + } + + foreach ($parsed['attachments'] as $idx => $att) { + $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', (string) $att['filename']); + if ($safeName === '') { + $safeName = "allegato_{$idx}.dat"; + } + $attPath = "{$attachmentsDir}/{$safeName}"; + @file_put_contents($attPath, $att['content']); + + $savedAttachments[] = [ + 'filename' => $att['filename'], + 'path' => $attPath, + 'content_type' => $att['content_type'], + 'size' => $att['size'], + 'is_inline' => $att['is_inline'], + ]; + } + } + + $senderDisplay = $parsed['from']['name'] ? "{$parsed['from']['name']} <{$parsed['from']['email']}>" : $parsed['from']['email']; + + $comm = CommunicationMessage::create([ + 'channel' => $channel, + 'direction' => 'inbound', + 'external_message_id' => $messageId, + 'stabile_id' => null, + 'sender_name' => $senderDisplay, + 'phone_number' => null, + 'message_text' => $parsed['body_text'] ?: strip_tags((string) $parsed['body_html']), + 'attachments' => $savedAttachments, + 'ticket_id' => null, + 'status' => 'received', + 'received_at' => $parsed['date'] ?: now(), + 'metadata' => [ + 'subject' => $parsed['subject'], + 'sender_email' => $parsed['from']['email'], + 'sender_name' => $parsed['from']['name'], + 'to' => $parsed['to'], + 'cc' => $parsed['cc'], + 'body_html' => $parsed['body_html'], + 'eml_path' => $emlFullPath, + 'is_pec' => $channel === 'pec', + 'is_studio' => true, + 'mailbox_label' => $mailboxConfig['label'] ?? 'Studio', + 'mailbox_email' => $mailboxConfig['email'] ?? '', + ], + ]); + + return [ + 'status' => 'imported', + 'id' => $comm->id, + 'channel' => $channel, + 'subject' => $parsed['subject'], + 'from' => $senderDisplay, + 'date' => $parsed['date']?->format('d/m/Y H:i'), + 'has_attachments' => count($savedAttachments) > 0, + ]; + } + /** * Ingesta un messaggio da stringa EML grezza */ diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php index 752de3c..d55a65c 100755 --- a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php @@ -1,18 +1,202 @@ -
-
- Questa area serve a verificare via web la prontezza dell'integrazione Google e della posta studio. La lettura automatica caselle non e ancora attiva come daemon applicativo, ma puoi validare configurazione OAuth e collegamento account senza entrare in shell. -
+
+ {{-- Google OAuth & General Studio Actions --}} +
+
+
+ 🌐 + Integrazione Google Workspace & Posta Studio +
+ Convalida rapida OAuth e sincronizzazione IMAP +
-
- Collega Google - Verifica Google OAuth - Scollega Google +
+ Verifica via web la prontezza dell'integrazione Google e delle caselle IMAP/PEC dello studio. Le credenziali salvate sopra vengono utilizzate per scaricare le comunicazioni in formato canonico .EML. +
+ +
+ + + 🔑 + Collega Account Google + + + + + + 🔍 + Verifica Google OAuth + + + + + + + Scollega Google + + + + + ✉️ + Webmail Ordinaria Studio + + + + 🛡️ + Webmail PEC Studio + +
@if(filled($this->opsLastOutput)) -
-
Ultimo output verifica posta / Google
-
{{ $this->opsLastOutput }}
+
+
Ultimo output verifica posta / Google
+
{{ $this->opsLastOutput }}
@endif + + {{-- Stabili Mailboxes Overview Table (All 16 Stabili) --}} + @php + $stabiliStatus = $this->stabiliMailboxStatus; + @endphp + +
+
+
+ 📬 +
+
Stato Posta e PEC degli Stabili Gestiti
+
Quadro di configurazione e monitoraggio messaggi per tutti i {{ count($stabiliStatus) }} stabili
+
+
+ +
+ + {{ count($stabiliStatus) }} Stabili Attivi + +
+
+ +
+ + + + + + + + + + + + + @forelse($stabiliStatus as $stb) + + + + + + + + + + + + + + @empty + + + + @endforelse + +
StabileCodice FiscalePosta Ordinaria (IMAP)Posta Certificata (PEC)MessaggiAzioni Rapide
+
+ {{ $stb['codice_stabile'] }} + {{ $stb['denominazione'] }} +
+
{{ $stb['comune'] ?: '—' }}
+
+ {{ $stb['codice_fiscale'] ?: '—' }} + + @if($stb['email_configured']) +
+ + Configurata +
+ @if($stb['email_info']) +
+ {{ $stb['email_info'] }} +
+ @endif + @else + + + Non configurata + + @endif +
+ @if($stb['pec_configured']) +
+ + Configurata +
+ @if($stb['pec_info']) +
+ {{ $stb['pec_info'] }} +
+ @endif + @else + + + Non configurata + + @endif +
+ @if($stb['msg_count'] > 0) + + {{ $stb['msg_count'] }} + + @else + 0 + @endif + + +
+ Nessuno stabile accessibile censito. +
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/posta/webmail.blade.php b/resources/views/filament/pages/posta/webmail.blade.php index 3a38d5f..15cf019 100644 --- a/resources/views/filament/pages/posta/webmail.blade.php +++ b/resources/views/filament/pages/posta/webmail.blade.php @@ -39,6 +39,15 @@ class="w-full py-2 px-3 text-xs rounded-xl bg-gray-50 dark:bg-gray-900 border bo
@empty -
+
-
Nessun messaggio presente in questa cartella
-

- Usa il pulsante "Sincronizza IMAP (.EML)" in alto per scaricare la posta dalle caselle ufficiali degli stabili. +

Nessun messaggio presente in questa cartella
+

+ Se non hai ancora configurato la casella IMAP o PEC, puoi inserire parametri e credenziali per abilitare lo scaricamento e la visualizzazione automatica.

+
@endforelse
diff --git a/resources/views/filament/pages/supporto/ticket-gestione.blade.php b/resources/views/filament/pages/supporto/ticket-gestione.blade.php index eeb35af..70d56cb 100755 --- a/resources/views/filament/pages/supporto/ticket-gestione.blade.php +++ b/resources/views/filament/pages/supporto/ticket-gestione.blade.php @@ -1,132 +1,288 @@ -
-
-
- Vai a Inserimento Ticket +
+ {{-- KPI Summary Stats Header --}} +
+
+
+ Aperti + 🎟️ +
+
{{ $ticketCounters['open'] ?? 0 }}
+
Ticket in attesa o assegnati
-
-
Aperti: {{ $ticketCounters['open'] ?? 0 }}
-
Urgenti: {{ $ticketCounters['urgent'] ?? 0 }}
-
Chiusi: {{ $ticketCounters['closed'] ?? 0 }}
-
Totali: {{ $ticketCounters['all'] ?? 0 }}
+ +
+
+ Urgenti + 🚨 +
+
{{ $ticketCounters['urgent'] ?? 0 }}
+
Priorità alta o emergenza
+
+ +
+
+ Chiusi + +
+
{{ $ticketCounters['closed'] ?? 0 }}
+
Interventi completati e risolti
+
+ +
+
+ Totali + 📊 +
+
{{ $ticketCounters['all'] ?? 0 }}
+
Volume storico complessivo
-
-
-
- - - -
- @if($activeTab === 'elenco') - - @endif + +
+ @if($activeTab === 'elenco') +
+ Stato: + +
+ @endif + + + + Nuovo Ticket + +
@if($activeTab === 'elenco') -
-
Categorie ticket
-
- - - +
+
+ + +
+
+ + +
-
- Aggiungi categoria - Modifica categoria selezionata + +
+ + @if($selectedCategoriaId) + + @endif
-
- - - - - - - - - - - - + {{-- Table View --}} +
+
IDTitoloCategoriaTipo interventoAssegnato aPrioritaStatoAperturaAzioni
+ + + + + + + + + + - + @forelse($tickets as $ticket) - - - + + + - - - - - - - + + + + + + + + + + @empty - + @endforelse diff --git a/resources/views/filament/pages/unita-immobiliare.blade.php b/resources/views/filament/pages/unita-immobiliare.blade.php index 0573380..56740bb 100755 --- a/resources/views/filament/pages/unita-immobiliare.blade.php +++ b/resources/views/filament/pages/unita-immobiliare.blade.php @@ -1119,17 +1119,31 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-xs font-semibol -
IDTitolo & AllegatiCategoriaAssegnato aPrioritàStatoData AperturaAzioni
#{{ (int) $ticket->id }} -
+ @php + $prio = strtolower((string) $ticket->priorita); + $prioBadge = match($prio) { + 'urgente', 'emergenza' => 'bg-rose-100 text-rose-800 dark:bg-rose-950/60 dark:text-rose-300 border-rose-200', + 'alta' => 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border-amber-200', + 'media' => 'bg-sky-100 text-sky-800 dark:bg-sky-950/60 dark:text-sky-300 border-sky-200', + default => 'bg-slate-100 text-slate-700 dark:bg-gray-800 dark:text-gray-300 border-slate-200', + }; + + $stato = strtolower((string) $ticket->stato); + $statoBadge = match($stato) { + 'aperto' => 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300 border-blue-200', + 'preso in carico', 'in lavorazione' => 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border-amber-200', + 'risolto', 'completato' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300 border-emerald-200', + 'chiuso' => 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400 border-gray-300', + default => 'bg-slate-100 text-slate-800 border-slate-200', + }; + @endphp +
+ #{{ (int) $ticket->id }} + +
{{ $ticket->titolo }} @if(((int) ($ticket->attachments_count ?? 0)) > 0) - - Allegati {{ (int) $ticket->attachments_count }} + + 📎 {{ (int) $ticket->attachments_count }} @endif
+ @if($ticket->stabile) +
+ {{ $ticket->stabile->codice_stabile }} - {{ $ticket->stabile->denominazione }} +
+ @endif
{{ optional($ticket->categoriaTicket)->nome ?: '-' }}{{ $this->getTipoInterventoLabel($ticket) }}{{ optional($ticket->assegnatoAUser)->name ?: '-' }}{{ $ticket->priorita }}{{ $ticket->stato }}{{ optional($ticket->data_apertura)->format('d/m/Y H:i') }} -
- - @if((int) ($ticket->assegnato_a_fornitore_id ?? 0) > 0) - - @endif +
+ {{ optional($ticket->categoriaTicket)->nome ?: '—' }} + + {{ optional($ticket->assegnatoAUser)->name ?: (optional($ticket->assegnatoAFornitore)->ragione_sociale ?: '—') }} + + + {{ $ticket->priorita }} + + + + {{ $ticket->stato }} + + + {{ optional($ticket->data_apertura)->format('d/m/Y H:i') ?: '—' }} + +
+ @if(in_array($ticket->stato, ['Aperto'], true)) - + @endif + @if(in_array($ticket->stato, ['Aperto', 'Preso in Carico'], true)) - + @endif + @if(in_array($ticket->stato, ['Aperto', 'Preso in Carico', 'In Lavorazione', 'In Attesa Approvazione', 'In Attesa Ricambi'], true)) - + @endif + @if(in_array($ticket->stato, ['Risolto'], true)) - + @endif
Nessun ticket per il filtro selezionato. + Nessun ticket presente per il filtro selezionato. +
{{ $prot['importo'] !== null ? '€ ' . number_format($prot['importo'], 2, ',', '.') : '—' }} - @if(!empty($prot['nome_file'])) - + + @php + $allegatiList = !empty($prot['allegati']) ? $prot['allegati'] : (!empty($prot['nome_file']) ? [['filename' => $prot['nome_file'], 'path' => $prot['file_path'], 'exists' => $prot['file_exists'], 'is_pdf' => str_ends_with(strtolower($prot['nome_file']), '.pdf'), 'is_image' => preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $prot['nome_file'])]] : []); + @endphp + @if(!empty($allegatiList)) +
+ @foreach($allegatiList as $att) + @php + $attName = $att['filename'] ?? 'file'; + $attPath = $att['path'] ?? ''; + $attExists = $att['exists'] ?? false; + $isPdf = $att['is_pdf'] ?? str_ends_with(strtolower($attName), '.pdf'); + $isImg = $att['is_image'] ?? preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $attName); + @endphp + + @endforeach +
@else @endif diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 3751430..4941bd1 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -6,30 +6,22 @@ # CURRENT-205 ## Obiettivo Completato -1. **Gestione Fornitore Nethome SAS (`10055221005`) & Collegamento Compensi Stabili**: - - Fornitore ID 236 (`NETHOME sas di BARONE M. & C.`, P.IVA / CF `10055221005`) censito e collegato trasversalmente a tutti i 16 stabili in `fornitore_stabile_impostazioni`. - - Verificate le 5 fatture elettroniche già presenti a sistema con Nethome come fornitore/cedente. +1. **Gestione Posta Studio & 16 Stabili (IMAP, Webmail, Scheda Amministratore)**: + - Sviluppato supporto per caselle di posta e PEC dello **Studio Amministratore** (`stabile_id = null`, storage in `storage/app/private/amministratori/{cod_adm}/studio/posta_{ordinaria|pec}/{anno}/`). + - Webmail Posta Ordinaria e PEC dotate di filtro rapido per Stabile o Studio, pulsante per configurazione diretta IMAP e gestione stati cartelle vuote. + - Nella Scheda Amministratore (Tab Posta / API): implementata la tabella di monitoraggio live di tutti i 16 stabili con conteggi messaggi `.eml`, stato configurazione IMAP/PEC, link diretti alla configurazione e pulsanti di test/sincronizzazione per le caselle di studio. -2. **Codice Mnemonico `cod_stabile` da `Stabili.mdb`**: - - Riallineati e popolati i codici mnemonici su tutti i 16 stabili target (`0002` -> `908`, `0008` -> `909`, `0009` -> `912`, `0010` -> `13`, `0011` -> `910`, `0012` -> `920`, `0013` -> `17`, `0016` -> `145`, `0017` -> `907`, `0018` -> `18`, `0019` -> `146`, `0021` -> `148`, `0022` -> `928`, `0023` -> `8`, `0024` -> `147`, `0025` -> `223`). - - Visibile e ricercabile nella tabella `http://192.168.0.205:8000/admin-filament/gescon/anagrafica/stabili`. +2. **Risoluzione Multi-Allegato & Timeline Protocollo (`ArchiveFileResolver`)**: + - Creato il resolver euristico `ArchiveFileResolver` per individuare file legacy e moderni tra radici storage e cartelle (`E_C/`, `INC_EC/`, `WA/`, `XML/`, `allegati/`, `posta/`, `documenti/`). + - Aggiornata la scheda Unità Immobiliare (Tab Comunicazioni & Protocollo) con supporto a `posta_dett`, `corrisp_inviata`, `protoc_ec` e `communication_messages`, con rendering di chip allegati dedicati con icone, badge di presenza ed anteprima modale istantanea. -3. **Integrazione IMAP e Archiviazione Locale `.EML` (`php artisan gescon:fetch-mail`)**: - - Sviluppato `ImapClient` in puro PHP SSL/TLS su porta 993 (zero dipendenze binarie libc-client) con supporto comandi IMAP standard (LOGIN, SELECT, SEARCH, FETCH BODY). - - Sviluppato `EmlParser` per parsing MIME conforme RFC 822/2822/5322 con decodifica multipart, body HTML/testo e salvataggio allegati. - - Salvataggio automatico del file grezzo `.eml` in `storage/app/private/amministratori/{cod_adm}/stabili/{cod_stabile}/posta_{ordinaria|pec}/{anno}/`. - - Creazione del record in `communication_messages` e auto-matching dell'Unità Immobiliare per email del mittente. - - Supporto configurazione e test connessione IMAP direttamente dalla scheda stabile (`/admin-filament/condomini/stabile?tab=posta-ufficiale`). +3. **Modernizzazione Ticket Gestione & Hub Documentale**: + - `TicketGestione`: KPI statistiche in testata, selettore filtri pill-bar, gestione rapida categorie, badge priorità/stato, filtri per fornitore operativo e gestione pratiche assicurative. + - `DocumentiArchivio`: Hub documentale centralizzato digitale e fisico, con anteprime QR, gestione template Drive e codici mnemonici `DOC-XXXX` / `AF-X-XXXX`. -4. **Due Nuove Blade Webmail Stile Gmail (Posta Ordinaria e Posta PEC)**: - - `PostaOrdinariaWebmail` (`http://192.168.0.205:8000/admin-filament/comunicazioni/posta-ordinaria`) - - `PostaPecWebmail` (`http://192.168.0.205:8000/admin-filament/comunicazioni/posta-pec`) - - Layout responsive modellato fedelmente sullo screenshot Gmail: barra di ricerca in tempo reale con debounce, filtro per stabile singolo o aggregato, pillole allegati (PDF, immagini, documenti), contrassegno speciale ⭐, apertura 1-click di ticket condominiale, composizione modale ed anteprima allegati rapida. - -5. **Scheda Unità (`unita-immobiliare`) - Tab Comunicazioni & Protocollo**: - - Aggregazione completa di corrispondenza storica (`protoc_ec`, `corrisp_inviata`) e messaggi email/PEC associati all'unità (`communication_messages`). - - Ordinamento cronologico rigorosamente decrescente basato su Unix timestamp per evitare anomalie di ordinamento lessicografico. - - Modal di anteprima visiva istantanea di PDF, immagini e documenti allegati cliccando sul nome del file. +4. **Fornitore Nethome SAS (`10055221005`) & Codici Stabili**: + - Censito e associato trasversalmente per compensi e fatturazione su tutti i 16 stabili. + - Codici mnemonici `cod_stabile` sincronizzati da `Stabili.mdb`. ## Output del Giro Operativo @@ -37,8 +29,9 @@ ## Output del Giro Operativo TASK_ID: task-nethome-imap-webmail-protocollo REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: d7f8346 +COMMIT: 3265562 FILE_O_AREE_TOCCATE: +- app/Services/Documenti/ArchiveFileResolver.php - app/Services/Posta/EmlParser.php - app/Services/Posta/ImapClient.php - app/Services/Posta/ImapMailboxService.php @@ -46,23 +39,24 @@ ## Output del Giro Operativo - app/Filament/Pages/Posta/PostaOrdinariaWebmail.php - app/Filament/Pages/Posta/PostaPecWebmail.php - resources/views/filament/pages/posta/webmail.blade.php +- app/Filament/Pages/Impostazioni/SchedaAmministratore.php +- resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php - app/Filament/Pages/Condomini/StabilePage.php - resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php - app/Filament/Pages/UnitaImmobiliarePage.php - resources/views/filament/pages/unita-immobiliare.blade.php -- app/Models/PersonaUnitaRelazione.php -- skill-netgescon/directives/gestione-posta-imap-webmail-protocollo.md +- resources/views/filament/pages/supporto/ticket-gestione.blade.php - tests/Feature/PostaImapWebmailAndProtocolloTest.php - skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (34 passed, 210 assertions) +- ./vendor/bin/pest tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (35 passed, 223 assertions) GATE_STATISTICS: -- NETHOME_SAS_FORNITORE: Fornitore 236 configurato e collegato a tutti i 16 stabili. -- COD_STABILE_MNEMONICO: 16 stabili aggiornati con cod_stabile da Stabili.mdb. -- IMAP_EML_SYNC: Client socket puro PHP SSL/TLS + Parser RFC 822 + archiviazione .eml in storage per anno/stabile. -- GMAIL_WEBMAIL_UI: Due blade dedicate (Posta Ordinaria e PEC) conformi allo screenshot di riferimento. -- UNITA_PROTOCOLLO_MODAL: Ordinamento cronologico Unix decrescente + modal anteprima allegati/documenti. -- TEST_SUITE: 34 test Feature passati con successo (210 asserzioni, 100% pass). +- STUDIO_MAILBOX_INGESTION: Gestione caselle posta/PEC studio e storage locale dedicato. +- STABILI_MAILBOX_MONITORING: Monitoraggio tabellare dei 16 stabili nella Scheda Amministratore con link diretti. +- MULTI_ATTACHMENT_RESOLVER: `ArchiveFileResolver` operativo su 7 cartelle e storage. +- WEBMAIL_UI_QUICK_ACTIONS: Bottone Configura IMAP, badge e filtri responsive. +- TICKET_GESTIONE_MODERNIZATION: KPI cards, filtri di stato, gestione categorie e assegnazioni. +- TEST_SUITE: 35 test Feature passati con successo (223 asserzioni, 100% pass). BLOCCO_DATI: no BLOCCO_CONTRATTO: no RISCHI_APERTI: nessuno @@ -70,5 +64,5 @@ ## Output del Giro Operativo ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero`. -- Eseguire la suite di test Pest (34 passed, 210 assertions). -- Verificare la Webmail Posta Ordinaria su `http://192.168.0.205:8000/admin-filament/comunicazioni/posta-ordinaria`, la Webmail PEC su `http://192.168.0.205:8000/admin-filament/comunicazioni/posta-pec` e la Scheda Unità (tab Comunicazioni & Protocollo) su `http://192.168.0.205:8000/admin-filament/unita-immobiliare?tab=preferenze`. +- Eseguire la suite di test Pest (35 passed, 223 assertions). +- Verificare la Webmail Posta Ordinaria (`/admin-filament/comunicazioni/posta-ordinaria`), PEC (`/admin-filament/comunicazioni/posta-pec`), Scheda Amministratore (`/admin-filament/impostazioni/amministratore?tab=posta-api`), Ticket Gestione (`/admin-filament/supporto/ticket-gestione`) e Scheda Unità (tab Comunicazioni & Protocollo). diff --git a/tests/Feature/PostaImapWebmailAndProtocolloTest.php b/tests/Feature/PostaImapWebmailAndProtocolloTest.php index 5501faf..269955d 100644 --- a/tests/Feature/PostaImapWebmailAndProtocolloTest.php +++ b/tests/Feature/PostaImapWebmailAndProtocolloTest.php @@ -165,3 +165,44 @@ expect($stabile0016->fresh()->cod_stabile)->toBe('145'); } }); + +it('ingests studio raw eml without stabile_id and resolves via ArchiveFileResolver', function () { + $user = User::factory()->create(); + Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']); + $user->assignRole('amministratore'); + + $adm = \App\Models\Amministratore::firstOrCreate( + ['codice_amministratore' => 'ADMX3PD9'], + ['user_id' => $user->id, 'nome' => 'Cecilia', 'cognome' => 'Tordini'] + ); + + $service = app(ImapMailboxService::class); + $rawEml = "From: Studio Legale \r\n" + . "To: studio@amministrazione.it\r\n" + . "Subject: Notifica studio generale\r\n" + . "Date: Fri, 04 Sep 2026 16:00:00 +0200\r\n" + . "Message-ID: \r\n" + . "MIME-Version: 1.0\r\n" + . "Content-Type: text/plain; charset=UTF-8\r\n" + . "\r\n" + . "Comunicazione indirizzata direttamente allo studio amministrativo."; + + $res = $service->ingestStudioEmlString($rawEml, $adm, [ + 'tipo' => 'imap', + 'folder' => 'INBOX', + 'crea_ticket_automatico' => false, + ], false); + + expect($res['status'])->toBe('imported'); + + $msg = CommunicationMessage::where('external_message_id', 'studio-msg-777@example.com')->first(); + expect($msg)->not->toBeNull(); + expect($msg->stabile_id)->toBeNull(); + expect($msg->metadata['is_studio'] ?? false)->toBeTrue(); + + // Test ArchiveFileResolver + $resolved = \App\Services\Documenti\ArchiveFileResolver::resolve('0016', 'sample_non_existing.pdf'); + expect($resolved)->toHaveKeys(['filename', 'exists', 'path', 'size', 'mime', 'is_pdf', 'is_image']); + expect($resolved['filename'])->toBe('sample_non_existing.pdf'); + expect($resolved['exists'])->toBeFalse(); +});