Add stable communications hub and Germanico 79 water sync
This commit is contained in:
parent
7c74942d0f
commit
016177bce0
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages\Condomini;
|
||||
|
||||
use App\Filament\Pages\Impostazioni\GoogleDashboard;
|
||||
use App\Filament\Pages\Strumenti\Comunicazioni;
|
||||
use App\Models\CommunicationMessage;
|
||||
use App\Models\Documento;
|
||||
use App\Models\DocumentoCollegato;
|
||||
use App\Models\InsuranceClaim;
|
||||
use App\Models\InsurancePolicy;
|
||||
|
|
@ -9,6 +13,8 @@
|
|||
use App\Models\Ticket;
|
||||
use App\Models\TicketAttachment;
|
||||
use App\Models\User;
|
||||
use App\Services\Support\GmailTicketImportService;
|
||||
use App\Support\GoogleAccountStore;
|
||||
use App\Support\Livewire\SupportsRubricaLookup;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
|
|
@ -16,6 +22,7 @@
|
|||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\WithFileUploads;
|
||||
|
|
@ -75,6 +82,18 @@ class StabilePage extends Page
|
|||
|
||||
public ?int $selectedInsurancePolicyId = null;
|
||||
|
||||
public string $officialPecCondominio = '';
|
||||
|
||||
public string $officialPecStudio = '';
|
||||
|
||||
public int $gmailImportMaxMessages = 10;
|
||||
|
||||
/** @var array<int,array<string,mixed>> */
|
||||
public array $officialMailboxes = [];
|
||||
|
||||
/** @var array<string,string> */
|
||||
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<int,mixed> $rows
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
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<int,array<string,mixed>>
|
||||
*/
|
||||
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<string,mixed>
|
||||
*/
|
||||
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<int,string>
|
||||
*/
|
||||
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<string,mixed>
|
||||
*/
|
||||
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<int,CommunicationMessage>
|
||||
*/
|
||||
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<string,mixed>|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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Strumenti;
|
||||
|
||||
use App\Filament\Pages\Condomini\StabilePage;
|
||||
use App\Filament\Pages\Supporto\TicketGestione;
|
||||
use App\Models\CommunicationMessage;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Comunicazioni extends Page
|
||||
{
|
||||
|
|
@ -19,10 +25,164 @@ class Comunicazioni extends Page
|
|||
|
||||
protected string $view = 'filament.pages.strumenti.comunicazioni';
|
||||
|
||||
public ?string $stabileId = null;
|
||||
|
||||
public string $channel = 'all';
|
||||
|
||||
public string $queue = 'all';
|
||||
|
||||
public array $stabileOptions = [];
|
||||
|
||||
public array $stats = [
|
||||
'totale' => 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']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ public function panel(Panel $panel): Panel
|
|||
]);
|
||||
}
|
||||
|
||||
protected function resolveSupplierCatalogUrl(): string
|
||||
private function resolveSupplierCatalogUrl(): string
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 <amministratore_id> --mdb=/mnt/gescon-archives/gescon/dbc/Fornitori.mdb`
|
||||
|
|
|
|||
110
docs/support/GERMANICO79-POSTA-ACQUA-2026-04.md
Normal file
110
docs/support/GERMANICO79-POSTA-ACQUA-2026-04.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<div class="space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Documenti</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->documentazioneStats['documenti'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Archivio documenti dello stabile.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Documenti collegati</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->documentazioneStats['documenti_collegati'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Contratti, polizze e protocolli.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Comunicazioni</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->documentazioneStats['comunicazioni'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Messaggi e tracciatura eventi.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Cartelle Drive template</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ count($this->driveTemplateFolders) }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Struttura pronta per il passaggio consegne.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-sky-200 bg-sky-50 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-sky-900">Hub documentale dello stabile</div>
|
||||
<div class="mt-1 text-xs text-sky-800">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.</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ $this->getGoogleSettingsUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50">Google e Drive</a>
|
||||
<a href="{{ $this->getStabilePostaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50">Configurazione posta completa</a>
|
||||
<a href="{{ $this->getGlobalCommunicationsUrl() }}" class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-800">Regia comunicazioni</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Casella ufficiale</div>
|
||||
<div class="mt-2 text-sm text-slate-900">{{ collect($this->officialMailboxes)->firstWhere('enabled', true)['email'] ?? 'Nessuna casella attiva' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Per GERMANICO 79 la configurazione proposta usa viagermanico79@gmail.com.</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Account Google dedicato</div>
|
||||
<div class="mt-2 text-sm text-slate-900">{{ $this->officialGoogleAccount['email'] ?? 'Da collegare' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Chiave prevista: stabile-{{ (int) ($stabile->id ?? 0) }}-pec</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Template cartelle Drive condominio</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Struttura base replicabile per ogni stabile e riutilizzabile anche in fase di passaggio consegne.</div>
|
||||
<div class="mt-4 grid gap-2 md:grid-cols-2 xl:grid-cols-3">
|
||||
@foreach($this->driveTemplateFolders as $folder)
|
||||
<div class="rounded-lg border bg-slate-50 px-3 py-2 text-sm text-slate-700">{{ $folder }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="mb-4 text-sm font-semibold text-slate-900">Gestione documentale avanzata</div>
|
||||
@include('admin.stabili.tabs.gestione-documentale', ['stabile' => $stabile])
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="mb-4 text-sm font-semibold text-slate-900">Documenti collegati e protocolli</div>
|
||||
@include('admin.stabili.tabs.documenti-collegati', ['stabile' => $stabile])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<div class="space-y-6">
|
||||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-emerald-900">Posta ufficiale dello stabile</div>
|
||||
<div class="mt-1 text-xs text-emerald-800">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.</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ $this->getStableGoogleConnectUrl() }}" class="inline-flex items-center rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-500">Collega account Google dello stabile</a>
|
||||
<a href="{{ $this->getGoogleSettingsUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50">Impostazioni Google</a>
|
||||
<a href="{{ $this->getStabilePostaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50">Vista completa posta</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-[1.15fr,0.85fr]">
|
||||
<div class="rounded-xl border bg-white p-4 space-y-4">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-slate-700">Email/PEC ufficiale del condominio</span>
|
||||
<input type="email" wire:model.defer="officialPecCondominio" class="w-full rounded-lg border-slate-300" placeholder="pec o email ufficiale dello stabile" />
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-slate-700">PEC o email studio per questo stabile</span>
|
||||
<input type="email" wire:model.defer="officialPecStudio" class="w-full rounded-lg border-slate-300" placeholder="canale studio dedicato a questo stabile" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<label class="block text-sm w-40">
|
||||
<span class="mb-1 block font-medium text-slate-700">Max email per import</span>
|
||||
<input type="number" min="1" max="50" wire:model.defer="gmailImportMaxMessages" class="w-full rounded-lg border-slate-300" />
|
||||
</label>
|
||||
<x-filament::button type="button" color="gray" wire:click="saveOfficialMailSettings">Salva impostazioni</x-filament::button>
|
||||
<x-filament::button type="button" color="primary" wire:click="importOfficialGmail">Importa Gmail ora</x-filament::button>
|
||||
<x-filament::button type="button" color="success" wire:click="addOfficialMailbox">Aggiungi casella</x-filament::button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@forelse($officialMailboxes as $index => $mailbox)
|
||||
<div class="rounded-xl border p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-slate-900">Casella {{ $index + 1 }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<x-filament::button type="button" size="sm" color="primary" wire:click="importOfficialGmail({{ $index }})">Importa</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="danger" wire:click="removeOfficialMailbox({{ $index }})">Rimuovi</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" wire:model.defer="officialMailboxes.{{ $index }}.enabled" class="rounded border-slate-300" />
|
||||
Attiva casella
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Tipo</span>
|
||||
<select wire:model.defer="officialMailboxes.{{ $index }}.tipo" class="w-full rounded-lg border-slate-300">
|
||||
<option value="gmail">Gmail / Google Workspace</option>
|
||||
<option value="imap">IMAP</option>
|
||||
<option value="pec">PEC via IMAP</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Etichetta</span>
|
||||
<input type="text" wire:model.defer="officialMailboxes.{{ $index }}.label" class="w-full rounded-lg border-slate-300" />
|
||||
</label>
|
||||
<label class="block text-sm md:col-span-2 xl:col-span-1">
|
||||
<span class="mb-1 block font-medium">Email casella</span>
|
||||
<input type="email" wire:model.defer="officialMailboxes.{{ $index }}.email" class="w-full rounded-lg border-slate-300" />
|
||||
</label>
|
||||
<label class="block text-sm md:col-span-2 xl:col-span-2">
|
||||
<span class="mb-1 block font-medium">Account Google collegato</span>
|
||||
<select wire:model.defer="officialMailboxes.{{ $index }}.google_account_key" class="w-full rounded-lg border-slate-300">
|
||||
<option value="">Seleziona account collegato</option>
|
||||
@foreach($stableGoogleAccountOptions as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-sm md:col-span-2 xl:col-span-3">
|
||||
<span class="mb-1 block font-medium">Query Gmail</span>
|
||||
<input type="text" wire:model.defer="officialMailboxes.{{ $index }}.gmail_query" class="w-full rounded-lg border-slate-300" placeholder="es. in:inbox newer_than:30d" />
|
||||
</label>
|
||||
<label class="block text-sm md:col-span-2 xl:col-span-3">
|
||||
<span class="mb-1 block font-medium">Mittenti autorizzati</span>
|
||||
<input type="text" wire:model.defer="officialMailboxes.{{ $index }}.mittenti_autorizzati" class="w-full rounded-lg border-slate-300" placeholder="email separate da virgola" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed p-6 text-sm text-slate-500">Nessuna casella configurata. Aggiungi la prima casella ufficiale dello stabile.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Riepilogo attivazione</div>
|
||||
<div class="mt-3 space-y-2 text-sm">
|
||||
<div><span class="font-medium">Stabile:</span> {{ $stabile->denominazione }}</div>
|
||||
<div><span class="font-medium">Codice:</span> {{ $stabile->codice_stabile }}</div>
|
||||
<div><span class="font-medium">Casella proposta:</span> {{ collect($officialMailboxes)->firstWhere('enabled', true)['email'] ?? 'Nessuna' }}</div>
|
||||
<div><span class="font-medium">PEC condominio:</span> {{ $officialPecCondominio !== '' ? $officialPecCondominio : '-' }}</div>
|
||||
<div><span class="font-medium">Account Google:</span> {{ $this->officialGoogleAccount['email'] ?? 'Da collegare' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Note operative</div>
|
||||
<div class="mt-2 text-xs leading-5 text-slate-500">
|
||||
La chiave account prevista per questo stabile è <strong>stabile-{{ (int) ($stabile->id ?? 0) }}-pec</strong>.
|
||||
Dopo il collegamento Google, la casella Gmail può essere usata per import ticket, archiviazione allegati e protocollo comunicazioni.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<div class="space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Totale</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->communicationStats['totale'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Eventi protocollati sullo stabile.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Email</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->communicationStats['email'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Canale email registrato.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Da lavorare</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->communicationStats['da_lavorare'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Messaggi senza ticket o non chiusi.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Oggi</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->communicationStats['oggi'] }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">Movimenti registrati in giornata.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-amber-900">Protocollo comunicazioni dello stabile</div>
|
||||
<div class="mt-1 text-xs text-amber-800">Questo protocollo tiene lo storico per stabile ma resta compatibile con una regia amministratore unica, come già succede per i ticket.</div>
|
||||
</div>
|
||||
<a href="{{ $this->getGlobalCommunicationsUrl() }}" class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-800">Apri regia amministratore</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white overflow-hidden">
|
||||
<div class="border-b px-4 py-3 text-sm font-semibold text-slate-900">Ultime comunicazioni dello stabile</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Protocollo</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Canale</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Mittente</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Messaggio</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Stato</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Ticket</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Ricevuto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
@forelse($this->communicationRows as $message)
|
||||
<tr>
|
||||
<td class="px-4 py-3 align-top font-mono text-xs text-slate-700">{{ $message->protocol_number }}</td>
|
||||
<td class="px-4 py-3 align-top">{{ strtoupper((string) $message->channel) }}</td>
|
||||
<td class="px-4 py-3 align-top">{{ $message->sender_name ?: '-' }}</td>
|
||||
<td class="px-4 py-3 align-top text-slate-600">{{ \Illuminate\Support\Str::limit((string) ($message->message_text ?? ''), 120) }}</td>
|
||||
<td class="px-4 py-3 align-top">{{ $message->status ?: '-' }}</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
@if($message->ticket_id)
|
||||
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(['ticket' => (int) $message->ticket_id, 'tab' => 'scheda'], panel: 'admin-filament') }}" class="text-primary-600 hover:underline">#{{ (int) $message->ticket_id }}</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top text-slate-500">{{ optional($message->received_at)->format('d/m/Y H:i') ?: '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-10 text-center text-sm text-slate-500">Nessuna comunicazione protocollata per questo stabile.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,101 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-medium">Comunicazioni</div>
|
||||
<div class="text-sm text-gray-500">Sezione in preparazione.</div>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-slate-900">Regia comunicazioni</div>
|
||||
<div class="text-sm text-slate-600">Vista unica per amministratore: protocollo per stabile, casistiche di studio e messaggi che devono ancora diventare ticket o lavorazione.</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<select wire:model.live="stabileId" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">Tutti gli stabili</option>
|
||||
@foreach($stabileOptions as $id => $label)
|
||||
<option value="{{ $id }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select wire:model.live="channel" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="all">Tutti i canali</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="phone">Telefono</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
</select>
|
||||
<select wire:model.live="queue" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="all">Tutto</option>
|
||||
<option value="attenzione">Da lavorare</option>
|
||||
<option value="ticket">Con ticket</option>
|
||||
<option value="studio">Solo studio</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Totale</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['totale'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Email</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['email'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Da lavorare</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['attenzione'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Studio</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['studio'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white overflow-hidden">
|
||||
<div class="border-b px-4 py-3 text-sm font-semibold text-slate-900">Ultime comunicazioni</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Protocollo</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Stabile</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Canale</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Mittente</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Messaggio</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Stato</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Ticket</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-slate-600">Ricevuto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
@forelse($rows as $row)
|
||||
<tr>
|
||||
<td class="px-4 py-3 align-top font-mono text-xs text-slate-700">{{ $row['protocol_number'] }}</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
@if($row['stabile_url'])
|
||||
<a href="{{ $row['stabile_url'] }}" class="text-primary-600 hover:underline">{{ $row['stabile_label'] }}</a>
|
||||
@else
|
||||
<span class="text-slate-500">{{ $row['stabile_label'] }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">{{ strtoupper($row['channel']) }}</td>
|
||||
<td class="px-4 py-3 align-top">{{ $row['sender_name'] }}</td>
|
||||
<td class="px-4 py-3 align-top text-slate-600">{{ $row['message_text'] }}</td>
|
||||
<td class="px-4 py-3 align-top">{{ $row['status'] }}</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
@if($row['ticket_url'])
|
||||
<a href="{{ $row['ticket_url'] }}" class="text-primary-600 hover:underline">#{{ $row['ticket_id'] }}</a>
|
||||
@else
|
||||
<span class="text-slate-500">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top text-slate-500">{{ $row['received_at'] }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-10 text-center text-sm text-slate-500">Nessuna comunicazione trovata con i filtri attivi.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user