From 016177bce0a822d7155e5da1a4020abac7d0da97 Mon Sep 17 00:00:00 2001 From: michele Date: Sat, 11 Apr 2026 13:41:41 +0000 Subject: [PATCH] Add stable communications hub and Germanico 79 water sync --- CHANGELOG.md | 3 + app/Filament/Pages/Condomini/StabilePage.php | 359 +++++++++++++++++- app/Filament/Pages/Condomini/StabilePosta.php | 50 ++- .../Pages/Strumenti/Comunicazioni.php | 164 +++++++- .../Filament/AdminFilamentPanelProvider.php | 2 +- docs/README.md | 1 + docs/support/AGGIORNAMENTI-STAGING-GIT.md | 19 + .../GERMANICO79-POSTA-ACQUA-2026-04.md | 110 ++++++ docs/support/MANUALE-SVILUPPO-OPERATIVO.md | 12 + .../condomini/tabs/documentazione.blade.php | 73 ++++ .../condomini/tabs/posta-ufficiale.blade.php | 117 ++++++ .../tabs/protocollo-comunicazioni.blade.php | 76 ++++ .../pages/strumenti/comunicazioni.blade.php | 101 ++++- 13 files changed, 1048 insertions(+), 39 deletions(-) create mode 100644 docs/support/GERMANICO79-POSTA-ACQUA-2026-04.md create mode 100644 resources/views/filament/pages/condomini/tabs/documentazione.blade.php create mode 100644 resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php create mode 100644 resources/views/filament/pages/condomini/tabs/protocollo-comunicazioni.blade.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8247027..25092ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ # Changelog ## [Unreleased] +- Added stable-level Documentazione, Posta ufficiale and Protocollo comunicazioni tabs, plus a real admin-wide Comunicazioni regia page reusing Google accounts, Gmail import, Drive templates and protocol records. +- Seeded the official Gmail path for stabile `0023` `GERMANICO 79` (`viagermanico79@gmail.com`), hardened PEC studio persistence against the missing `pec_amministratore` DB column, and linked the new stable documentation hub to the existing Google settings workflow. +- Applied `gescon:auto-sync-anagrafiche` for `GERMANICO 79` with stable/unit role linking and imported the 2024 legacy water dataset from `generale_stabile.mdb` into servizi/letture without closing historic years. - Initial open-source release prep. - Reorganized the supplier scheda into operational tabs, linked supplier rubrica-clienti back to anagrafica unica, fixed the supplier ticket-intervento Blade parse error, and hardened legacy supplier matching plus SMDR/Post-it labeling for Git-first rollout. - Reworked Supporto > Aggiornamento Nodo to show local/remote commit dates, explicit pending commit counts, and a clear note that Git updates do not move storage archives or private uploaded data. diff --git a/app/Filament/Pages/Condomini/StabilePage.php b/app/Filament/Pages/Condomini/StabilePage.php index f27bbd4..f5697eb 100644 --- a/app/Filament/Pages/Condomini/StabilePage.php +++ b/app/Filament/Pages/Condomini/StabilePage.php @@ -1,6 +1,10 @@ > */ + public array $officialMailboxes = []; + + /** @var array */ + public array $stableGoogleAccountOptions = []; + public string $insuranceBrokerSearch = ''; public string $insuranceCompanySearch = ''; @@ -196,6 +215,8 @@ public function mount(): void if (! array_key_exists($this->tab, $this->tabs())) { $this->tab = 'dati-generali'; } + + $this->initializeOfficialMailSettings(); } /** @@ -204,32 +225,34 @@ public function mount(): void public function tabs(): array { return [ - 'dati-generali' => 'Dati generali', - 'unita-immobiliari' => 'Unità / Locali tecnici', - 'palazzine' => 'Palazzine', - 'assicurazioni' => 'Assicurazioni', - 'tabelle-millesimali' => 'Tabelle millesimali', - 'rate-emesse' => 'Rate emesse', - 'gestioni' => 'Gestioni', - 'gestione-documentale' => 'Gestione documentale', - 'documenti-collegati' => 'Documenti collegati', - 'dati-bancari' => 'Banche e casse', + 'dati-generali' => 'Dati generali', + 'unita-immobiliari' => 'Unità / Locali tecnici', + 'palazzine' => 'Palazzine', + 'assicurazioni' => 'Assicurazioni', + 'tabelle-millesimali' => 'Tabelle millesimali', + 'rate-emesse' => 'Rate emesse', + 'gestioni' => 'Gestioni', + 'documentazione' => 'Documentazione', + 'posta-ufficiale' => 'Posta ufficiale', + 'protocollo-comunicazioni' => 'Protocollo comunicazioni', + 'dati-bancari' => 'Banche e casse', ]; } public function tabView(): string { return match ($this->tab) { - 'rate-emesse' => 'admin.stabili.tabs.rate-emesse', - 'palazzine' => 'admin.stabili.tabs.palazzine', - 'assicurazioni' => 'filament.pages.condomini.tabs.assicurazioni', - 'dati-bancari' => 'filament.pages.condomini.tabs.dati-bancari', - 'tabelle-millesimali' => 'filament.pages.condomini.tabelle-millesimali-tab', - 'gestioni' => 'admin.stabili.tabs.gestioni', - 'unita-immobiliari' => 'admin.stabili.tabs.unita-immobiliari', - 'gestione-documentale' => 'admin.stabili.tabs.gestione-documentale', - 'documenti-collegati' => 'admin.stabili.tabs.documenti-collegati', - default => 'admin.stabili.tabs.dati-generali', + 'rate-emesse' => 'admin.stabili.tabs.rate-emesse', + 'palazzine' => 'admin.stabili.tabs.palazzine', + 'assicurazioni' => 'filament.pages.condomini.tabs.assicurazioni', + 'dati-bancari' => 'filament.pages.condomini.tabs.dati-bancari', + 'tabelle-millesimali' => 'filament.pages.condomini.tabelle-millesimali-tab', + 'gestioni' => 'admin.stabili.tabs.gestioni', + 'unita-immobiliari' => 'admin.stabili.tabs.unita-immobiliari', + 'documentazione' => 'filament.pages.condomini.tabs.documentazione', + 'posta-ufficiale' => 'filament.pages.condomini.tabs.posta-ufficiale', + 'protocollo-comunicazioni' => 'filament.pages.condomini.tabs.protocollo-comunicazioni', + default => 'admin.stabili.tabs.dati-generali', }; } @@ -993,4 +1016,300 @@ private function extractLegacyRateMonthsFromConfig(array $config, string $sectio return $this->normalizeMesi($months); } + + private function initializeOfficialMailSettings(): void + { + if (! $this->stabile instanceof StabileModel) { + $this->officialMailboxes = []; + return; + } + + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $posta = (array) ($config['posta'] ?? []); + + $this->stableGoogleAccountOptions = $this->stabile->amministratore + ? app(GoogleAccountStore::class)->options($this->stabile->amministratore) + : []; + $this->officialPecCondominio = trim((string) ($this->stabile->pec_condominio ?? ($posta['pec_condominio'] ?? ''))); + $this->officialPecStudio = trim((string) ($posta['pec_studio'] ?? (Schema::hasColumn('stabili', 'pec_amministratore') ? ($this->stabile->pec_amministratore ?? '') : ''))); + $this->officialMailboxes = $this->normalizeOfficialMailboxes((array) ($posta['caselle'] ?? [])); + + if ($this->officialMailboxes === []) { + $this->officialMailboxes = $this->buildDefaultOfficialMailboxes(); + } + } + + /** + * @param array $rows + * @return array> + */ + private function normalizeOfficialMailboxes(array $rows): array + { + $normalized = []; + + foreach ($rows as $row) { + if (! is_array($row)) { + continue; + } + + $normalized[] = [ + 'enabled' => (bool) ($row['enabled'] ?? true), + 'tipo' => strtolower(trim((string) ($row['tipo'] ?? 'gmail'))) ?: 'gmail', + 'label' => trim((string) ($row['label'] ?? '')), + 'google_account_key' => trim((string) ($row['google_account_key'] ?? '')), + 'email' => trim((string) ($row['email'] ?? '')), + 'host' => trim((string) ($row['host'] ?? '')), + 'port' => (int) ($row['port'] ?? 993), + 'encryption' => trim((string) ($row['encryption'] ?? 'ssl')) ?: 'ssl', + 'username' => trim((string) ($row['username'] ?? '')), + 'password' => trim((string) ($row['password'] ?? '')), + 'folder' => trim((string) ($row['folder'] ?? 'INBOX')) ?: 'INBOX', + 'gmail_query' => trim((string) ($row['gmail_query'] ?? '')), + 'crea_ticket_automatico' => (bool) ($row['crea_ticket_automatico'] ?? true), + 'mittenti_autorizzati' => trim((string) ($row['mittenti_autorizzati'] ?? '')), + ]; + } + + return $normalized; + } + + /** + * @return array> + */ + private function buildDefaultOfficialMailboxes(): array + { + if (! $this->stabile instanceof StabileModel) { + return []; + } + + if ((string) ($this->stabile->codice_stabile ?? '') === '0023') { + return [[ + 'enabled' => true, + 'tipo' => 'gmail', + 'label' => 'Casella ufficiale GERMANICO 79', + 'google_account_key' => 'stabile-' . (int) $this->stabile->id . '-pec', + 'email' => 'viagermanico79@gmail.com', + 'host' => '', + 'port' => 993, + 'encryption' => 'ssl', + 'username' => 'viagermanico79@gmail.com', + 'password' => '', + 'folder' => 'INBOX', + 'gmail_query' => 'in:inbox newer_than:30d', + 'crea_ticket_automatico' => true, + 'mittenti_autorizzati' => '', + ]]; + } + + return []; + } + + public function addOfficialMailbox(): void + { + $this->officialMailboxes[] = [ + 'enabled' => true, + 'tipo' => 'gmail', + 'label' => '', + 'google_account_key' => '', + 'email' => '', + 'host' => '', + 'port' => 993, + 'encryption' => 'ssl', + 'username' => '', + 'password' => '', + 'folder' => 'INBOX', + 'gmail_query' => '', + 'crea_ticket_automatico' => true, + 'mittenti_autorizzati' => '', + ]; + } + + public function removeOfficialMailbox(int $index): void + { + unset($this->officialMailboxes[$index]); + $this->officialMailboxes = array_values($this->officialMailboxes); + } + + public function saveOfficialMailSettings(): void + { + if (! $this->stabile instanceof StabileModel) { + return; + } + + $mailboxes = array_values(array_filter($this->normalizeOfficialMailboxes($this->officialMailboxes), function (array $row): bool { + return $row['label'] !== '' || $row['email'] !== '' || $row['google_account_key'] !== ''; + })); + + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config['posta'] = array_merge([ + 'acquisizione' => [ + 'salva_documenti' => true, + 'salva_contabilita' => true, + 'mittenti_assicurazione' => '', + 'descrizione_default' => 'Documento acquisito da casella stabile', + ], + ], (array) ($config['posta'] ?? []), [ + 'pec_condominio' => $this->officialPecCondominio !== '' ? $this->officialPecCondominio : null, + 'pec_studio' => $this->officialPecStudio !== '' ? $this->officialPecStudio : null, + 'caselle' => $mailboxes, + ]); + + $this->stabile->pec_condominio = $this->officialPecCondominio !== '' ? $this->officialPecCondominio : null; + if (Schema::hasColumn('stabili', 'pec_amministratore')) { + $this->stabile->pec_amministratore = $this->officialPecStudio !== '' ? $this->officialPecStudio : null; + } + $this->stabile->configurazione_avanzata = $config; + $this->stabile->save(); + + $this->officialMailboxes = $mailboxes; + + Notification::make() + ->title('Posta ufficiale dello stabile salvata') + ->success() + ->send(); + } + + public function importOfficialGmail(?int $index = null): void + { + if (! $this->stabile instanceof StabileModel) { + return; + } + + $mailboxes = $this->normalizeOfficialMailboxes($this->officialMailboxes); + if ($index !== null) { + $mailboxes = isset($mailboxes[$index]) ? [$mailboxes[$index]] : []; + } + + $mailboxes = array_values(array_filter($mailboxes, function (array $mailbox): bool { + return (bool) ($mailbox['enabled'] ?? false) + && strtolower((string) ($mailbox['tipo'] ?? '')) === 'gmail'; + })); + + if ($mailboxes === []) { + Notification::make()->title('Nessuna casella Gmail attiva da importare')->warning()->send(); + return; + } + + $results = []; + foreach ($mailboxes as $mailbox) { + $results[] = app(GmailTicketImportService::class)->importMailbox( + $this->stabile, + $mailbox, + max(1, min((int) $this->gmailImportMaxMessages, 50)) + ); + } + + $summary = collect($results) + ->map(fn(array $row): string => ($row['mailbox'] ?? 'gmail') . ': ' . (int) ($row['imported'] ?? 0) . ' importate, ' . (int) ($row['skipped'] ?? 0) . ' saltate, ' . (int) ($row['attachments'] ?? 0) . ' allegati') + ->implode(' | '); + + Notification::make() + ->title('Import Gmail completato') + ->body($summary !== '' ? $summary : 'Nessun nuovo messaggio importato.') + ->success() + ->send(); + } + + public function getGoogleSettingsUrl(): string + { + return GoogleDashboard::getUrl(panel: 'admin-filament'); + } + + public function getStabilePostaUrl(): string + { + return StabilePosta::getUrl(['stabile_id' => (int) ($this->stabile?->id ?? 0)], panel: 'admin-filament'); + } + + public function getStableGoogleConnectUrl(): string + { + $label = 'PEC ' . ($this->stabile?->denominazione ?: ('stabile-' . ($this->stabile?->id ?? 0))); + + return route('oauth.google.redirect', [ + 'account_key' => 'stabile-' . ($this->stabile?->id ?? 0) . '-pec', + 'label' => $label, + 'return_to' => request()->getRequestUri(), + ]); + } + + public function getGlobalCommunicationsUrl(): string + { + return Comunicazioni::getUrl(panel: 'admin-filament') . '?stabile_id=' . (int) ($this->stabile?->id ?? 0); + } + + /** + * @return array + */ + public function getDocumentazioneStatsProperty(): array + { + $stabileId = (int) ($this->stabile?->id ?? 0); + if ($stabileId <= 0) { + return ['documenti' => 0, 'documenti_collegati' => 0, 'comunicazioni' => 0, 'caselle' => 0]; + } + + return [ + 'documenti' => Documento::query()->where('stabile_id', $stabileId)->count(), + 'documenti_collegati' => DocumentoCollegato::query()->where('stabile_id', $stabileId)->count(), + 'comunicazioni' => CommunicationMessage::query()->where('stabile_id', $stabileId)->count(), + 'caselle' => count(array_filter($this->officialMailboxes, fn(array $row): bool => (bool) ($row['enabled'] ?? false))), + ]; + } + + /** + * @return array + */ + public function getDriveTemplateFoldersProperty(): array + { + $folders = config('netgescon.google.drive_template_folders', []); + + return is_array($folders) + ? array_values(array_filter(array_map('strval', $folders), fn(string $row) : bool => trim($row) !== '')) + : []; + } + + /** + * @return array + */ + public function getCommunicationStatsProperty(): array + { + $stabileId = (int) ($this->stabile?->id ?? 0); + $base = CommunicationMessage::query()->where('stabile_id', $stabileId); + + return [ + 'totale' => (clone $base)->count(), + 'email' => (clone $base)->where('channel', 'email')->count(), + 'da_lavorare' => (clone $base)->where(function ($query): void { + $query->whereNull('ticket_id') + ->orWhereNotIn('status', ['linked_to_ticket', 'processed', 'archived']); + })->count(), + 'oggi' => (clone $base)->whereDate('received_at', now()->toDateString())->count(), + ]; + } + + /** + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getCommunicationRowsProperty() + { + $stabileId = (int) ($this->stabile?->id ?? 0); + + return CommunicationMessage::query() + ->with(['ticket', 'stabile']) + ->where('stabile_id', $stabileId) + ->orderByDesc('received_at') + ->orderByDesc('id') + ->limit(30) + ->get(); + } + + /** + * @return array|null + */ + public function getOfficialGoogleAccountProperty(): ?array + { + if (! $this->stabile?->amministratore) { + return null; + } + + return app(GoogleAccountStore::class)->get($this->stabile->amministratore, 'stabile-' . (int) $this->stabile->id . '-pec'); + } } diff --git a/app/Filament/Pages/Condomini/StabilePosta.php b/app/Filament/Pages/Condomini/StabilePosta.php index 85d8470..9d3a88a 100644 --- a/app/Filament/Pages/Condomini/StabilePosta.php +++ b/app/Filament/Pages/Condomini/StabilePosta.php @@ -3,8 +3,8 @@ use App\Models\Amministratore; use App\Models\Stabile; -use App\Support\GoogleAccountStore; use App\Models\User; +use App\Support\GoogleAccountStore; use App\Support\StabileContext; use BackedEnum; use Filament\Forms\Components\Repeater; @@ -19,6 +19,7 @@ use Filament\Schemas\Schema; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Schema as SchemaFacade; use UnitEnum; class StabilePosta extends Page implements HasForms @@ -65,18 +66,38 @@ public function mount(): void $stabile = StabileContext::accessibleStabili($user)->firstWhere('id', $stabileId); abort_unless($stabile instanceof Stabile, 404); - $this->stabile = $stabile; + $this->stabile = $stabile; $this->googleAccountOptions = $stabile->amministratore instanceof Amministratore ? app(GoogleAccountStore::class)->options($stabile->amministratore) : []; - $posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []); + $posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []); + $caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array')); + + if ($caselle === [] && (string) ($stabile->codice_stabile ?? '') === '0023') { + $caselle = [[ + 'enabled' => true, + 'tipo' => 'gmail', + 'label' => 'Casella ufficiale GERMANICO 79', + 'google_account_key' => 'stabile-' . (int) $stabile->id . '-pec', + 'email' => 'viagermanico79@gmail.com', + 'host' => '', + 'port' => 993, + 'encryption' => 'ssl', + 'username' => 'viagermanico79@gmail.com', + 'password' => '', + 'folder' => 'INBOX', + 'gmail_query' => 'in:inbox newer_than:30d', + 'crea_ticket_automatico' => true, + 'mittenti_autorizzati' => '', + ]]; + } $this->getSchema('form')?->fill([ 'pec_condominio' => (string) ($stabile->pec_condominio ?? ''), - 'pec_amministratore' => (string) ($stabile->pec_amministratore ?? ''), + 'pec_amministratore' => (string) (($posta['pec_studio'] ?? (SchemaFacade::hasColumn('stabili', 'pec_amministratore') ? ($stabile->pec_amministratore ?? '') : '')) ?? ''), 'posta' => array_merge([ - 'caselle' => [], + 'caselle' => $caselle, 'acquisizione' => [ 'salva_documenti' => true, 'salva_contabilita' => true, @@ -130,8 +151,8 @@ public function form(Schema $schema): Schema ->maxLength(120), Select::make('google_account_key') ->label('Account Google collegato') - ->options(fn (): array => $this->googleAccountOptions) - ->visible(fn (callable $get): bool => strtolower((string) $get('tipo')) === 'gmail') + ->options(fn(): array=> $this->googleAccountOptions) + ->visible(fn(callable $get): bool => strtolower((string) $get('tipo')) === 'gmail') ->helperText('Per Gmail seleziona l’account Google autorizzato che leggera questa casella.'), TextInput::make('email') ->label('Email casella') @@ -169,7 +190,7 @@ public function form(Schema $schema): Schema Toggle::make('crea_ticket_automatico') ->label('Crea ticket automatici dai nuovi messaggi') ->default(true) - ->visible(fn (callable $get): bool => strtolower((string) $get('tipo')) === 'gmail'), + ->visible(fn(callable $get): bool => strtolower((string) $get('tipo')) === 'gmail'), TextInput::make('mittenti_autorizzati') ->label('Mittenti ammessi') ->maxLength(500) @@ -203,12 +224,15 @@ public function save(): void { abort_unless($this->stabile instanceof Stabile, 404); - $state = is_array($this->data ?? null) ? $this->data : []; - $config = (array) ($this->stabile->configurazione_avanzata ?? []); - $config['posta'] = is_array($state['posta'] ?? null) ? $state['posta'] : []; + $state = is_array($this->data ?? null) ? $this->data : []; + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config['posta'] = is_array($state['posta'] ?? null) ? $state['posta'] : []; + $config['posta']['pec_studio'] = trim((string) ($state['pec_amministratore'] ?? '')) ?: null; - $this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null; - $this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null; + $this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null; + if (SchemaFacade::hasColumn('stabili', 'pec_amministratore')) { + $this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null; + } $this->stabile->configurazione_avanzata = $config; $this->stabile->save(); diff --git a/app/Filament/Pages/Strumenti/Comunicazioni.php b/app/Filament/Pages/Strumenti/Comunicazioni.php index 3950f14..9678726 100644 --- a/app/Filament/Pages/Strumenti/Comunicazioni.php +++ b/app/Filament/Pages/Strumenti/Comunicazioni.php @@ -1,11 +1,17 @@ 0, + 'email' => 0, + 'attenzione' => 0, + 'studio' => 0, + ]; + + public array $rows = []; + + public function mount(): void + { + $user = Auth::user(); + + if (! $user instanceof User) { + abort(403); + } + + $this->stabileOptions = StabileContext::accessibleStabili($user) + ->sortBy('denominazione') + ->mapWithKeys(fn(Stabile $stabile) => [ + (string) $stabile->id => trim(($stabile->codice_stabile ?: '-') . ' - ' . ($stabile->denominazione ?: 'Stabile')), + ]) + ->all(); + + $requestedStabileId = request()->query('stabile'); + if (is_scalar($requestedStabileId) && array_key_exists((string) $requestedStabileId, $this->stabileOptions)) { + $this->stabileId = (string) $requestedStabileId; + } + + $requestedChannel = request()->query('channel'); + if (is_string($requestedChannel) && in_array($requestedChannel, ['all', 'email', 'phone', 'whatsapp'], true)) { + $this->channel = $requestedChannel; + } + + $requestedQueue = request()->query('queue'); + if (is_string($requestedQueue) && in_array($requestedQueue, ['all', 'attenzione', 'ticket', 'studio'], true)) { + $this->queue = $requestedQueue; + } + + $this->refreshData(); + } + + public function updatedStabileId(): void + { + $this->refreshData(); + } + + public function updatedChannel(): void + { + $this->refreshData(); + } + + public function updatedQueue(): void + { + $this->refreshData(); + } + + protected function refreshData(): void + { + $query = $this->baseQuery(); + + $this->stats = [ + 'totale' => (clone $query)->count(), + 'email' => (clone $query)->where('channel', 'email')->count(), + 'attenzione' => (clone $query)->where(function (Builder $builder): void { + $builder + ->whereNull('ticket_id') + ->orWhereNull('status') + ->orWhere('status', '!=', 'closed'); + })->count(), + 'studio' => (clone $query)->whereNull('stabile_id')->count(), + ]; + + $this->rows = (clone $query) + ->with(['stabile:id,codice_stabile,denominazione']) + ->latest('received_at') + ->latest('id') + ->limit(60) + ->get() + ->map(function (CommunicationMessage $message): array { + $stabileLabel = $message->stabile + ? trim(($message->stabile->codice_stabile ?: '-') . ' - ' . ($message->stabile->denominazione ?: 'Stabile')) + : 'Studio / generale'; + + return [ + 'protocol_number' => (string) ($message->protocol_number ?: '-'), + 'stabile_label' => $stabileLabel, + 'stabile_url' => $message->stabile_id ? StabilePage::getUrl(['stabile' => (int) $message->stabile_id], panel : 'admin-filament'): null, + 'channel' => (string) ($message->channel ?: '-'), + 'sender_name' => (string) ($message->sender_name ?: '-'), + 'message_text' => Str::limit((string) ($message->message_text ?: ''), 140), + 'status' => (string) ($message->status ?: '-'), + 'ticket_id' => $message->ticket_id ? (int) $message->ticket_id : null, + 'ticket_url' => $message->ticket_id ? TicketGestione::getUrl(['ticket' => (int) $message->ticket_id, 'tab' => 'scheda'], panel : 'admin-filament'): null, + 'received_at' => optional($message->received_at)->format('d/m/Y H:i') ?: '-', + ]; + }) + ->all(); + } + + protected function baseQuery(): Builder + { + $user = Auth::user(); + $allowedIds = $user instanceof User + ? StabileContext::accessibleStabili($user)->pluck('id')->map(fn($value) => (int) $value)->all() + : []; + + $query = CommunicationMessage::query(); + + if ($allowedIds !== []) { + $query->where(function (Builder $builder) use ($allowedIds): void { + $builder->whereIn('stabile_id', $allowedIds) + ->orWhereNull('stabile_id'); + }); + } else { + $query->whereNull('stabile_id'); + } + + if ($this->stabileId !== null && $this->stabileId !== '') { + $query->where('stabile_id', (int) $this->stabileId); + } + + if ($this->channel !== 'all') { + $query->where('channel', $this->channel); + } + + if ($this->queue === 'attenzione') { + $query->where(function (Builder $builder): void { + $builder + ->whereNull('ticket_id') + ->orWhereNull('status') + ->orWhere('status', '!=', 'closed'); + }); + } + + if ($this->queue === 'ticket') { + $query->whereNotNull('ticket_id'); + } + + if ($this->queue === 'studio') { + $query->whereNull('stabile_id'); + } + + return $query; + } + public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index 26bf5af..70b3e7d 100644 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -133,7 +133,7 @@ public function panel(Panel $panel): Panel ]); } - protected function resolveSupplierCatalogUrl(): string + private function resolveSupplierCatalogUrl(): string { $user = Auth::user(); diff --git a/docs/README.md b/docs/README.md index f5275a0..120ef67 100755 --- a/docs/README.md +++ b/docs/README.md @@ -90,6 +90,7 @@ ## 🧭 Documentazione & Indici - `support/BUG-RISOLTI-E-VALIDAZIONI.md` – registro sintetico bug corretti, fix applicati e verifiche ancora da completare. - `support/CONTINUITA-SVILUPPO-AGENT.md` – procedura minima per mantenere allineati manuale, note commit e sviluppo con agent. - `support/FORNITORE-OPERATIVITA-ROADMAP-2026-04.md` – roadmap operativa fornitore/manutentore: rubrica unica, ticket interni, lavori ricorrenti, TecnoRepair, catalogo prodotti e seriali. +- `support/GERMANICO79-POSTA-ACQUA-2026-04.md` – runbook operativo per il blocco GERMANICO 79: Gmail ufficiale, sync anagrafiche, import acqua 2024 e prossima estensione ticket-letture. - `docs/automation/` – commit log mensile, summary giornalieri e protocollo handoff automatici. - `07-IMPORT-UNITA-IMMOBILIARI.md` – blueprint operativo per importare unità e collegare proprietari/inquilini. - `08-ANAGRAFICA-UNICA-UI.md` – specifica UX della scheda anagrafica unica e relativi widget condominiali. diff --git a/docs/support/AGGIORNAMENTI-STAGING-GIT.md b/docs/support/AGGIORNAMENTI-STAGING-GIT.md index cd9064b..27448dc 100644 --- a/docs/support/AGGIORNAMENTI-STAGING-GIT.md +++ b/docs/support/AGGIORNAMENTI-STAGING-GIT.md @@ -121,6 +121,25 @@ ## Rilascio operativo 2026-04-06 Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging. +### Stabili / Google / acqua 2026-04-11 + +- In `condomini/stabile` sono stati introdotti i tab `Documentazione`, `Posta ufficiale` e `Protocollo comunicazioni` per concentrare archivio, Drive template, caselle Gmail/PEC e storico protocollo per singolo stabile. +- In `strumenti/comunicazioni` la pagina placeholder e stata sostituita con una regia amministratore filtrabile per stabile, canale e coda operativa. +- Per `0023 - GERMANICO 79` e stata proposta la casella ufficiale `viagermanico79@gmail.com` con chiave account Google dedicata `stabile-24-pec`. +- Anagrafica `GERMANICO 79` riallineata con: + - `php artisan gescon:auto-sync-anagrafiche --stabile=0023 --link-ruolo --include-comproprietari --limit=300 --apply` + - esito: `inseriti 17`, `aggiornati 67`, `ruoli inseriti 43`, `ruoli aggiornati 41` +- Acqua `GERMANICO 79` riallineata con: + - `php artisan gescon:import-acqua-legacy --stabile=0023 --acqua-years=2024 --open-years=2024 --no-close-gestioni --no-interaction` + - esito: `35 letture 2024 aggiornate`, `30` collegate a unita, `5` ancora senza match, `41` nominativi acqua legacy presenti su `unita_immobiliare_nominativi` + +Validazione rapida consigliata dopo update staging: + +1. aprire `admin-filament/condomini/stabile?tab=posta-ufficiale` sullo stabile `GERMANICO 79` +2. verificare la proposta Gmail ufficiale e il link a `Impostazioni > Google` +3. controllare `admin-filament/condomini/servizi-utenze` e `admin-filament/condomini/letture-servizi` sul 2024 +4. isolare le `5` letture ancora senza unita per il prossimo fix di matching + ### Sync fornitori legacy 2026-04-08 - La procedura fornitori va eseguita con un solo entry point: `php artisan gescon:sync-fornitori-legacy --mdb=/mnt/gescon-archives/gescon/dbc/Fornitori.mdb` diff --git a/docs/support/GERMANICO79-POSTA-ACQUA-2026-04.md b/docs/support/GERMANICO79-POSTA-ACQUA-2026-04.md new file mode 100644 index 0000000..a103661 --- /dev/null +++ b/docs/support/GERMANICO79-POSTA-ACQUA-2026-04.md @@ -0,0 +1,110 @@ +# GERMANICO 79 - Posta ufficiale, anagrafica e acqua + +## Scopo + +Questo runbook fissa il punto raggiunto su `0023 - GERMANICO 79` e prepara il prossimo slice operativo sulla raccolta letture acqua con link pubblico associato a unita immobiliare. + +## Stato stabile + +- stabile locale: `id=24` +- amministratore: `id=5` +- codice stabile: `0023` +- denominazione: `GERMANICO 79` +- archivio legacy acqua: `/mnt/gescon-archives/gescon/0023/generale_stabile.mdb` + +## Blocco documentazione / Google / comunicazioni + +Sono stati introdotti in `condomini/stabile` tre tab nuovi: + +1. `Documentazione` +2. `Posta ufficiale` +3. `Protocollo comunicazioni` + +Obiettivo operativo: + +- concentrare archivio documentale e documenti collegati dello stabile +- collegare Gmail/Google/Drive allo stabile reale +- mantenere storico protocollo comunicazioni sia per stabile sia lato studio + +Casella proposta per il primo aggancio operativo: + +- email: `viagermanico79@gmail.com` +- chiave account Google prevista: `stabile-24-pec` + +## Sync anagrafica eseguito + +Comando applicato: + +```bash +php artisan gescon:auto-sync-anagrafiche --stabile=0023 --link-ruolo --include-comproprietari --limit=300 --apply +``` + +Esito: + +- contatti inseriti: `17` +- contatti aggiornati: `67` +- ruoli inseriti: `43` +- ruoli aggiornati: `41` + +Stato dopo la sync: + +- `rubrica_ruoli` sullo stabile: `55` +- `rubrica_universale` amministratore `5`: `1952` record +- `unita_immobiliare_nominativi` collegati allo stabile: `91` + +## Import acqua 2024 eseguito + +Comando applicato: + +```bash +php artisan gescon:import-acqua-legacy --stabile=0023 --acqua-years=2024 --open-years=2024 --no-close-gestioni --no-interaction +``` + +Riferimenti legacy usati: + +- `acqua_gen.rif_ute` +- `acqua_fatture.rif_ute` +- `acqua_dett.rif_ute` +- `acqua_dett.t_cod_cond` + +Esito operativo: + +- letture 2024 processate: `35` +- letture create: `0` +- letture aggiornate: `35` +- letture collegate a unita: `30` +- letture senza match unita: `5` +- nominativi legacy acqua presenti su `unita_immobiliare_nominativi`: `41` +- chiusure storiche: `0` per scelta prudente + +Nota su tariffe: + +- la parte `stabile_servizio_tariffe` viene saltata quando l ambiente richiede `fattura_elettronica_id` obbligatoria, dato non disponibile direttamente in `acqua_gen` + +## Stato attuale pronto per il prossimo slice + +Base gia disponibile: + +- anagrafica e ruoli di stabile riallineati +- letture acqua 2024 importate e visibili in archivio +- casella ufficiale Gmail impostabile dal tab stabile +- regia comunicazioni amministratore pronta per protocollo e follow-up + +## Prossimo slice: ticket-lettura acqua semplificato + +Direzione concordata: + +1. usare la struttura ticket gia esistente come base logica +2. limitare allegati a `1-2` foto +3. raccogliere `lettura attuale` e identificazione di chi invia +4. inviare richiesta massiva ai nominativi dello stabile con link univoco per unita anche senza autenticazione +5. distinguere in UI letture `fatte` e `non fatte` +6. permettere il `sollecito` sui mancanti +7. mostrare anche la `lettura precedente` disponibile dai dati importati + +Ordine consigliato di implementazione: + +1. risolvere i `5` match mancanti tra `acqua_dett` e `unita_immobiliari` +2. creare token/link pubblico per singola unita e singola campagna lettura +3. aggiungere tab stabile con lista letture richieste, ricevute e mancanti +4. riusare il workflow ticket solo per stato, note e immagini, senza allegati generici \ No newline at end of file diff --git a/docs/support/MANUALE-SVILUPPO-OPERATIVO.md b/docs/support/MANUALE-SVILUPPO-OPERATIVO.md index fcdbc74..adbabab 100644 --- a/docs/support/MANUALE-SVILUPPO-OPERATIVO.md +++ b/docs/support/MANUALE-SVILUPPO-OPERATIVO.md @@ -35,6 +35,18 @@ ## Effetto pratico ## Implementazioni recenti da tenere come riferimento +### 0-ter. Stabile come hub documentale e operativo per posta e acqua + +- `condomini/stabile` ora raccoglie in un unico punto documentazione, caselle ufficiali, protocollo comunicazioni e link a Google/Drive. +- `strumenti/comunicazioni` e diventata una regia amministratore effettiva invece di una vista placeholder. +- Per `0023 - GERMANICO 79` sono stati riallineati sia la parte anagrafica sia l archivio acqua 2024, cosi il prossimo slice puo partire dalla raccolta letture unita per unita invece che da import grezzi sparsi. + +Effetto pratico: + +- [U] lo stabile ha un punto unico per posta ufficiale, protocollo e archivio documentale +- [U] l amministratore vede le comunicazioni sia per stabile sia a livello studio con la stessa logica dei ticket +- [P] la futura raccolta letture acqua puo appoggiarsi a dati gia importati e ai nominativi riallineati + ### 0. Direzione di prodotto: NetGescon come spazio unico a ruoli e moduli - La direzione attuale non e piu quella di un gestionale chiuso solo per l amministratore condominiale. diff --git a/resources/views/filament/pages/condomini/tabs/documentazione.blade.php b/resources/views/filament/pages/condomini/tabs/documentazione.blade.php new file mode 100644 index 0000000..c7cbed6 --- /dev/null +++ b/resources/views/filament/pages/condomini/tabs/documentazione.blade.php @@ -0,0 +1,73 @@ +
+
+
+
Documenti
+
{{ $this->documentazioneStats['documenti'] }}
+
Archivio documenti dello stabile.
+
+
+
Documenti collegati
+
{{ $this->documentazioneStats['documenti_collegati'] }}
+
Contratti, polizze e protocolli.
+
+
+
Comunicazioni
+
{{ $this->documentazioneStats['comunicazioni'] }}
+
Messaggi e tracciatura eventi.
+
+
+
Cartelle Drive template
+
{{ count($this->driveTemplateFolders) }}
+
Struttura pronta per il passaggio consegne.
+
+
+ +
+
+
+
Hub documentale dello stabile
+
Da qui centralizziamo documenti, cartelle Drive, posta ufficiale e protocollo comunicazioni. Questo prepara anche il passaggio dello stabile da un amministratore all'altro con archivio e storico già ordinati.
+
+ +
+ +
+
+
Casella ufficiale
+
{{ collect($this->officialMailboxes)->firstWhere('enabled', true)['email'] ?? 'Nessuna casella attiva' }}
+
Per GERMANICO 79 la configurazione proposta usa viagermanico79@gmail.com.
+
+
+
Account Google dedicato
+
{{ $this->officialGoogleAccount['email'] ?? 'Da collegare' }}
+
Chiave prevista: stabile-{{ (int) ($stabile->id ?? 0) }}-pec
+
+
+
+ +
+
Template cartelle Drive condominio
+
Struttura base replicabile per ogni stabile e riutilizzabile anche in fase di passaggio consegne.
+
+ @foreach($this->driveTemplateFolders as $folder) +
{{ $folder }}
+ @endforeach +
+
+ +
+
+
Gestione documentale avanzata
+ @include('admin.stabili.tabs.gestione-documentale', ['stabile' => $stabile]) +
+ +
+
Documenti collegati e protocolli
+ @include('admin.stabili.tabs.documenti-collegati', ['stabile' => $stabile]) +
+
+
diff --git a/resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php b/resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php new file mode 100644 index 0000000..bc833ae --- /dev/null +++ b/resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php @@ -0,0 +1,117 @@ +
+
+
+
+
Posta ufficiale dello stabile
+
Qui agganciamo Gmail, PEC e account Google ufficiali del condominio per lavorare su dati reali, con documenti e comunicazioni sempre trasferibili in caso di cambio amministratore.
+
+ +
+
+ +
+
+
+ + +
+ +
+ + Salva impostazioni + Importa Gmail ora + Aggiungi casella +
+ +
+ @forelse($officialMailboxes as $index => $mailbox) +
+
+
Casella {{ $index + 1 }}
+
+ Importa + Rimuovi +
+
+ +
+ + + + + + + +
+
+ @empty +
Nessuna casella configurata. Aggiungi la prima casella ufficiale dello stabile.
+ @endforelse +
+
+ +
+
+
Riepilogo attivazione
+
+
Stabile: {{ $stabile->denominazione }}
+
Codice: {{ $stabile->codice_stabile }}
+
Casella proposta: {{ collect($officialMailboxes)->firstWhere('enabled', true)['email'] ?? 'Nessuna' }}
+
PEC condominio: {{ $officialPecCondominio !== '' ? $officialPecCondominio : '-' }}
+
Account Google: {{ $this->officialGoogleAccount['email'] ?? 'Da collegare' }}
+
+
+ +
+
Note operative
+
+ La chiave account prevista per questo stabile è stabile-{{ (int) ($stabile->id ?? 0) }}-pec. + Dopo il collegamento Google, la casella Gmail può essere usata per import ticket, archiviazione allegati e protocollo comunicazioni. +
+
+
+
+
diff --git a/resources/views/filament/pages/condomini/tabs/protocollo-comunicazioni.blade.php b/resources/views/filament/pages/condomini/tabs/protocollo-comunicazioni.blade.php new file mode 100644 index 0000000..105508b --- /dev/null +++ b/resources/views/filament/pages/condomini/tabs/protocollo-comunicazioni.blade.php @@ -0,0 +1,76 @@ +
+
+
+
Totale
+
{{ $this->communicationStats['totale'] }}
+
Eventi protocollati sullo stabile.
+
+
+
Email
+
{{ $this->communicationStats['email'] }}
+
Canale email registrato.
+
+
+
Da lavorare
+
{{ $this->communicationStats['da_lavorare'] }}
+
Messaggi senza ticket o non chiusi.
+
+
+
Oggi
+
{{ $this->communicationStats['oggi'] }}
+
Movimenti registrati in giornata.
+
+
+ +
+
+
+
Protocollo comunicazioni dello stabile
+
Questo protocollo tiene lo storico per stabile ma resta compatibile con una regia amministratore unica, come già succede per i ticket.
+
+ Apri regia amministratore +
+
+ +
+
Ultime comunicazioni dello stabile
+
+ + + + + + + + + + + + + + @forelse($this->communicationRows as $message) + + + + + + + + + + @empty + + + + @endforelse + +
ProtocolloCanaleMittenteMessaggioStatoTicketRicevuto
{{ $message->protocol_number }}{{ strtoupper((string) $message->channel) }}{{ $message->sender_name ?: '-' }}{{ \Illuminate\Support\Str::limit((string) ($message->message_text ?? ''), 120) }}{{ $message->status ?: '-' }} + @if($message->ticket_id) + #{{ (int) $message->ticket_id }} + @else + - + @endif + {{ optional($message->received_at)->format('d/m/Y H:i') ?: '-' }}
Nessuna comunicazione protocollata per questo stabile.
+
+
+
diff --git a/resources/views/filament/pages/strumenti/comunicazioni.blade.php b/resources/views/filament/pages/strumenti/comunicazioni.blade.php index ae9f768..db0d5a1 100644 --- a/resources/views/filament/pages/strumenti/comunicazioni.blade.php +++ b/resources/views/filament/pages/strumenti/comunicazioni.blade.php @@ -1,6 +1,101 @@ -
-
Comunicazioni
-
Sezione in preparazione.
+
+
+
+
+
Regia comunicazioni
+
Vista unica per amministratore: protocollo per stabile, casistiche di studio e messaggi che devono ancora diventare ticket o lavorazione.
+
+
+ + + +
+
+
+ +
+
+
Totale
+
{{ $stats['totale'] }}
+
+
+
Email
+
{{ $stats['email'] }}
+
+
+
Da lavorare
+
{{ $stats['attenzione'] }}
+
+
+
Studio
+
{{ $stats['studio'] }}
+
+
+ +
+
Ultime comunicazioni
+
+ + + + + + + + + + + + + + + @forelse($rows as $row) + + + + + + + + + + + @empty + + + + @endforelse + +
ProtocolloStabileCanaleMittenteMessaggioStatoTicketRicevuto
{{ $row['protocol_number'] }} + @if($row['stabile_url']) + {{ $row['stabile_label'] }} + @else + {{ $row['stabile_label'] }} + @endif + {{ strtoupper($row['channel']) }}{{ $row['sender_name'] }}{{ $row['message_text'] }}{{ $row['status'] }} + @if($row['ticket_url']) + #{{ $row['ticket_id'] }} + @else + - + @endif + {{ $row['received_at'] }}
Nessuna comunicazione trovata con i filtri attivi.
+
+