feat(posta,protocollo,ticket): studio imap mailboxes, stabili monitoring, multi-attachment resolution and ticket ui modernization
This commit is contained in:
parent
e7e60ec45d
commit
d05865cb4d
|
|
@ -2094,4 +2094,117 @@ private function safeAmministratoreImpostazioni(Amministratore $amministratore):
|
|||
|
||||
return is_array($settings) ? $settings : [];
|
||||
}
|
||||
|
||||
public function getStabiliMailboxStatusProperty(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
$stabili = $user instanceof User ? StabileContext::accessibleStabili($user) : Stabile::all();
|
||||
$res = [];
|
||||
|
||||
foreach ($stabili as $s) {
|
||||
$cfg = (array) ($s->configurazione_avanzata ?? []);
|
||||
$posta = (array) ($cfg['posta'] ?? []);
|
||||
$caselle = (array) ($posta['caselle'] ?? []);
|
||||
|
||||
$emailConfigured = false;
|
||||
$emailInfo = '';
|
||||
$pecConfigured = false;
|
||||
$pecInfo = '';
|
||||
|
||||
foreach ($caselle as $c) {
|
||||
if (! is_array($c) || empty($c['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$isPec = ($c['tipo'] ?? '') === 'pec';
|
||||
$hasConn = ! empty($c['host']) && ! empty($c['username']);
|
||||
if ($isPec) {
|
||||
if ($hasConn) {
|
||||
$pecConfigured = true;
|
||||
$pecInfo = ($c['email'] ?? $c['username'] ?? '') . ' (' . ($c['host'] ?? '') . ')';
|
||||
}
|
||||
} else {
|
||||
if ($hasConn) {
|
||||
$emailConfigured = true;
|
||||
$emailInfo = ($c['email'] ?? $c['username'] ?? '') . ' (' . ($c['host'] ?? '') . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$msgCount = CommunicationMessage::where('stabile_id', $s->id)->count();
|
||||
|
||||
$res[] = [
|
||||
'id' => (int) $s->id,
|
||||
'codice_stabile' => (string) $s->codice_stabile,
|
||||
'denominazione' => (string) $s->denominazione,
|
||||
'comune' => (string) $s->comune,
|
||||
'codice_fiscale' => (string) $s->codice_fiscale,
|
||||
'email_configured' => $emailConfigured,
|
||||
'email_info' => $emailInfo,
|
||||
'pec_configured' => $pecConfigured,
|
||||
'pec_info' => $pecInfo,
|
||||
'msg_count' => $msgCount,
|
||||
'config_url' => url('/admin-filament/condomini/stabile?stabile_id=' . $s->id . '&tab=posta-ufficiale'),
|
||||
'webmail_email_url' => url('/admin-filament/comunicazioni/posta-ordinaria?stabile_id=' . $s->id),
|
||||
'webmail_pec_url' => url('/admin-filament/comunicazioni/posta-pec?stabile_id=' . $s->id),
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function testStudioMailbox(int $index): void
|
||||
{
|
||||
$caselle = (array) data_get($this->data, 'impostazioni.posta.caselle', []);
|
||||
$box = $caselle[$index] ?? null;
|
||||
|
||||
if (! is_array($box)) {
|
||||
Notification::make()->title('Configurazione casella non trovata')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(\App\Services\Posta\ImapMailboxService::class);
|
||||
$res = $service->testMailbox($box);
|
||||
|
||||
if ($res['success']) {
|
||||
Notification::make()
|
||||
->title('Connessione IMAP Studio Riuscita!')
|
||||
->body($res['message'])
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title('Test Connessione Fallito')
|
||||
->body($res['message'])
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function syncStudioMailbox(int $index): void
|
||||
{
|
||||
$caselle = (array) data_get($this->data, 'impostazioni.posta.caselle', []);
|
||||
$box = $caselle[$index] ?? null;
|
||||
|
||||
if (! is_array($box)) {
|
||||
Notification::make()->title('Configurazione casella non trovata')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(\App\Services\Posta\ImapMailboxService::class);
|
||||
$res = $service->fetchStudioMailbox($this->amministratore, $box, 30);
|
||||
|
||||
if (($res['imported'] ?? 0) > 0) {
|
||||
Notification::make()
|
||||
->title('Sincronizzazione completata')
|
||||
->body("Importati {$res['imported']} nuovi messaggi (.EML)")
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title('Nessun nuovo messaggio')
|
||||
->body($res['message'] ?? 'Nessun messaggio trovato da scaricare.')
|
||||
->info()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class PostaOrdinariaWebmail extends Page
|
|||
|
||||
public string $tipoCasella = 'email'; // 'email' o 'pec'
|
||||
|
||||
public ?int $selectedStabileId = null;
|
||||
public string|int|null $selectedStabileId = null;
|
||||
|
||||
public string $currentFolder = 'inbox'; // 'inbox', 'starred', 'sent', 'drafts', 'trash', 'attachments'
|
||||
|
||||
|
|
@ -68,9 +68,11 @@ public function mount(): void
|
|||
{
|
||||
$user = Auth::user();
|
||||
if ($user instanceof User) {
|
||||
$reqStabile = request()->integer('stabile_id');
|
||||
if ($reqStabile > 0) {
|
||||
$this->selectedStabileId = $reqStabile;
|
||||
$reqStabile = request()->query('stabile_id');
|
||||
if ($reqStabile === 'studio') {
|
||||
$this->selectedStabileId = 'studio';
|
||||
} elseif (is_numeric($reqStabile) && (int) $reqStabile > 0) {
|
||||
$this->selectedStabileId = (int) $reqStabile;
|
||||
} else {
|
||||
$this->selectedStabileId = StabileContext::resolveActiveStabileId($user);
|
||||
}
|
||||
|
|
@ -84,9 +86,66 @@ public function getStabiliOptionsProperty(): array
|
|||
return [];
|
||||
}
|
||||
|
||||
return StabileContext::accessibleStabili($user)
|
||||
$options = [
|
||||
'studio' => '🏢 Posta Generale Studio / Amministrazione',
|
||||
];
|
||||
|
||||
$stabili = StabileContext::accessibleStabili($user)
|
||||
->mapWithKeys(fn(Stabile $s) => [(int) $s->id => trim((string) ($s->codice_stabile . ' - ' . $s->denominazione))])
|
||||
->all();
|
||||
|
||||
return $options + $stabili;
|
||||
}
|
||||
|
||||
public function getConfigMailboxUrlProperty(): string
|
||||
{
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
return url('/admin-filament/impostazioni/scheda-amministratore?tab=posta-operativita');
|
||||
}
|
||||
|
||||
if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
return url('/admin-filament/condomini/stabile?stabile_id=' . $this->selectedStabileId . '&tab=posta-ufficiale');
|
||||
}
|
||||
|
||||
return url('/admin-filament/condomini/stabile?tab=posta-ufficiale');
|
||||
}
|
||||
|
||||
public function getIsMailboxConfiguredProperty(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$amministratore = $user->amministratore;
|
||||
if ($amministratore) {
|
||||
$config = (array) ($amministratore->impostazioni ?? []);
|
||||
$caselle = (array) ($config['posta']['caselle'] ?? []);
|
||||
foreach ($caselle as $c) {
|
||||
if (! empty($c['enabled']) && ! empty($c['host']) && ($this->tipoCasella === 'pec' ? ($c['tipo'] ?? '') === 'pec' : ($c['tipo'] ?? '') !== 'pec')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
$stabile = Stabile::find($this->selectedStabileId);
|
||||
if ($stabile) {
|
||||
$config = (array) ($stabile->configurazione_avanzata ?? []);
|
||||
$caselle = (array) ($config['posta']['caselle'] ?? []);
|
||||
foreach ($caselle as $c) {
|
||||
if (! empty($c['enabled']) && ! empty($c['host']) && ($this->tipoCasella === 'pec' ? ($c['tipo'] ?? '') === 'pec' : ($c['tipo'] ?? '') !== 'pec')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function selectFolder(string $folder): void
|
||||
|
|
@ -174,31 +233,56 @@ public function syncNow(): void
|
|||
}
|
||||
|
||||
$service = app(ImapMailboxService::class);
|
||||
$stabili = $this->selectedStabileId
|
||||
? Stabile::where('id', $this->selectedStabileId)->get()
|
||||
: StabileContext::accessibleStabili($user);
|
||||
|
||||
$totImported = 0;
|
||||
foreach ($stabili as $stabile) {
|
||||
$config = (array) ($stabile->configurazione_avanzata ?? []);
|
||||
$posta = (array) ($config['posta'] ?? []);
|
||||
$caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array'));
|
||||
|
||||
foreach ($caselle as $mailbox) {
|
||||
if (empty($mailbox['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap'));
|
||||
if ($this->tipoCasella === 'pec' && $mTipo !== 'pec') {
|
||||
continue;
|
||||
}
|
||||
if ($this->tipoCasella === 'email' && $mTipo === 'pec') {
|
||||
continue;
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$amministratore = $user->amministratore;
|
||||
if ($amministratore) {
|
||||
$config = (array) ($amministratore->impostazioni ?? []);
|
||||
$caselle = array_values(array_filter((array) ($config['posta']['caselle'] ?? []), 'is_array'));
|
||||
foreach ($caselle as $mailbox) {
|
||||
if (empty($mailbox['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap'));
|
||||
if ($this->tipoCasella === 'pec' && $mTipo !== 'pec') {
|
||||
continue;
|
||||
}
|
||||
if ($this->tipoCasella === 'email' && $mTipo === 'pec') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($mailbox['host']) && ! empty($mailbox['username'])) {
|
||||
$res = $service->fetchStudioMailbox($amministratore, $mailbox, 20);
|
||||
$totImported += (int) ($res['imported'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$stabili = (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0)
|
||||
? Stabile::where('id', (int) $this->selectedStabileId)->get()
|
||||
: StabileContext::accessibleStabili($user);
|
||||
|
||||
if (! empty($mailbox['host']) && ! empty($mailbox['username'])) {
|
||||
$res = $service->fetchMailbox($stabile, $mailbox, 20);
|
||||
$totImported += (int) ($res['imported'] ?? 0);
|
||||
foreach ($stabili as $stabile) {
|
||||
$config = (array) ($stabile->configurazione_avanzata ?? []);
|
||||
$posta = (array) ($config['posta'] ?? []);
|
||||
$caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array'));
|
||||
|
||||
foreach ($caselle as $mailbox) {
|
||||
if (empty($mailbox['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap'));
|
||||
if ($this->tipoCasella === 'pec' && $mTipo !== 'pec') {
|
||||
continue;
|
||||
}
|
||||
if ($this->tipoCasella === 'email' && $mTipo === 'pec') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! empty($mailbox['host']) && ! empty($mailbox['username'])) {
|
||||
$res = $service->fetchMailbox($stabile, $mailbox, 20);
|
||||
$totImported += (int) ($res['imported'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -283,8 +367,7 @@ public function getMessagesProperty()
|
|||
? StabileContext::accessibleStabili($user)->pluck('id')->toArray()
|
||||
: [];
|
||||
|
||||
$query = CommunicationMessage::with(['stabile', 'ticket'])
|
||||
->whereIn('stabile_id', $allowedStabileIds);
|
||||
$query = CommunicationMessage::with(['stabile', 'ticket']);
|
||||
|
||||
if ($this->tipoCasella === 'pec') {
|
||||
$query->where('channel', 'pec');
|
||||
|
|
@ -292,8 +375,15 @@ public function getMessagesProperty()
|
|||
$query->whereIn('channel', ['email', 'gmail']);
|
||||
}
|
||||
|
||||
if ($this->selectedStabileId) {
|
||||
$query->where('stabile_id', $this->selectedStabileId);
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$query->whereNull('stabile_id');
|
||||
} elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
$query->where('stabile_id', (int) $this->selectedStabileId);
|
||||
} else {
|
||||
$query->where(function ($q) use ($allowedStabileIds) {
|
||||
$q->whereIn('stabile_id', $allowedStabileIds)
|
||||
->orWhereNull('stabile_id');
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->currentFolder === 'sent') {
|
||||
|
|
@ -335,15 +425,22 @@ public function getFolderCountsProperty(): array
|
|||
? StabileContext::accessibleStabili($user)->pluck('id')->toArray()
|
||||
: [];
|
||||
|
||||
$base = CommunicationMessage::whereIn('stabile_id', $allowedStabileIds);
|
||||
$base = CommunicationMessage::query();
|
||||
if ($this->tipoCasella === 'pec') {
|
||||
$base->where('channel', 'pec');
|
||||
} else {
|
||||
$base->whereIn('channel', ['email', 'gmail']);
|
||||
}
|
||||
|
||||
if ($this->selectedStabileId) {
|
||||
$base->where('stabile_id', $this->selectedStabileId);
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$base->whereNull('stabile_id');
|
||||
} elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
$base->where('stabile_id', (int) $this->selectedStabileId);
|
||||
} else {
|
||||
$base->where(function ($q) use ($allowedStabileIds) {
|
||||
$q->whereIn('stabile_id', $allowedStabileIds)
|
||||
->orWhereNull('stabile_id');
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class PostaPecWebmail extends Page
|
|||
|
||||
public string $tipoCasella = 'pec';
|
||||
|
||||
public ?int $selectedStabileId = null;
|
||||
public string|int|null $selectedStabileId = null;
|
||||
|
||||
public string $currentFolder = 'inbox'; // 'inbox', 'starred', 'sent', 'drafts', 'trash', 'attachments'
|
||||
|
||||
|
|
@ -70,9 +70,11 @@ public function mount(): void
|
|||
{
|
||||
$user = Auth::user();
|
||||
if ($user instanceof User) {
|
||||
$reqStabile = request()->integer('stabile_id');
|
||||
if ($reqStabile > 0) {
|
||||
$this->selectedStabileId = $reqStabile;
|
||||
$reqStabile = request()->query('stabile_id');
|
||||
if ($reqStabile === 'studio') {
|
||||
$this->selectedStabileId = 'studio';
|
||||
} elseif (is_numeric($reqStabile) && (int) $reqStabile > 0) {
|
||||
$this->selectedStabileId = (int) $reqStabile;
|
||||
} else {
|
||||
$this->selectedStabileId = StabileContext::resolveActiveStabileId($user);
|
||||
}
|
||||
|
|
@ -86,9 +88,66 @@ public function getStabiliOptionsProperty(): array
|
|||
return [];
|
||||
}
|
||||
|
||||
return StabileContext::accessibleStabili($user)
|
||||
$options = [
|
||||
'studio' => '🏢 PEC Generale Studio / Amministrazione',
|
||||
];
|
||||
|
||||
$stabili = StabileContext::accessibleStabili($user)
|
||||
->mapWithKeys(fn(Stabile $s) => [(int) $s->id => trim((string) ($s->codice_stabile . ' - ' . $s->denominazione))])
|
||||
->all();
|
||||
|
||||
return $options + $stabili;
|
||||
}
|
||||
|
||||
public function getConfigMailboxUrlProperty(): string
|
||||
{
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
return url('/admin-filament/impostazioni/scheda-amministratore?tab=posta-operativita');
|
||||
}
|
||||
|
||||
if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
return url('/admin-filament/condomini/stabile?stabile_id=' . $this->selectedStabileId . '&tab=posta-ufficiale');
|
||||
}
|
||||
|
||||
return url('/admin-filament/condomini/stabile?tab=posta-ufficiale');
|
||||
}
|
||||
|
||||
public function getIsMailboxConfiguredProperty(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$amministratore = $user->amministratore;
|
||||
if ($amministratore) {
|
||||
$config = (array) ($amministratore->impostazioni ?? []);
|
||||
$caselle = (array) ($config['posta']['caselle'] ?? []);
|
||||
foreach ($caselle as $c) {
|
||||
if (! empty($c['enabled']) && ! empty($c['host']) && ($c['tipo'] ?? '') === 'pec') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
$stabile = Stabile::find($this->selectedStabileId);
|
||||
if ($stabile) {
|
||||
$config = (array) ($stabile->configurazione_avanzata ?? []);
|
||||
$caselle = (array) ($config['posta']['caselle'] ?? []);
|
||||
foreach ($caselle as $c) {
|
||||
if (! empty($c['enabled']) && ! empty($c['host']) && ($c['tipo'] ?? '') === 'pec') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function selectFolder(string $folder): void
|
||||
|
|
@ -176,28 +235,50 @@ public function syncNow(): void
|
|||
}
|
||||
|
||||
$service = app(ImapMailboxService::class);
|
||||
$stabili = $this->selectedStabileId
|
||||
? Stabile::where('id', $this->selectedStabileId)->get()
|
||||
: StabileContext::accessibleStabili($user);
|
||||
|
||||
$totImported = 0;
|
||||
foreach ($stabili as $stabile) {
|
||||
$config = (array) ($stabile->configurazione_avanzata ?? []);
|
||||
$posta = (array) ($config['posta'] ?? []);
|
||||
$caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array'));
|
||||
|
||||
foreach ($caselle as $mailbox) {
|
||||
if (empty($mailbox['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap'));
|
||||
if ($mTipo !== 'pec') {
|
||||
continue;
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$amministratore = $user->amministratore;
|
||||
if ($amministratore) {
|
||||
$config = (array) ($amministratore->impostazioni ?? []);
|
||||
$caselle = array_values(array_filter((array) ($config['posta']['caselle'] ?? []), 'is_array'));
|
||||
foreach ($caselle as $mailbox) {
|
||||
if (empty($mailbox['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap'));
|
||||
if ($mTipo !== 'pec') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($mailbox['host']) && ! empty($mailbox['username'])) {
|
||||
$res = $service->fetchStudioMailbox($amministratore, $mailbox, 20);
|
||||
$totImported += (int) ($res['imported'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$stabili = (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0)
|
||||
? Stabile::where('id', (int) $this->selectedStabileId)->get()
|
||||
: StabileContext::accessibleStabili($user);
|
||||
|
||||
if (! empty($mailbox['host']) && ! empty($mailbox['username'])) {
|
||||
$res = $service->fetchMailbox($stabile, $mailbox, 20);
|
||||
$totImported += (int) ($res['imported'] ?? 0);
|
||||
foreach ($stabili as $stabile) {
|
||||
$config = (array) ($stabile->configurazione_avanzata ?? []);
|
||||
$posta = (array) ($config['posta'] ?? []);
|
||||
$caselle = array_values(array_filter((array) ($posta['caselle'] ?? []), 'is_array'));
|
||||
|
||||
foreach ($caselle as $mailbox) {
|
||||
if (empty($mailbox['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$mTipo = strtolower((string) ($mailbox['tipo'] ?? 'imap'));
|
||||
if ($mTipo !== 'pec') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! empty($mailbox['host']) && ! empty($mailbox['username'])) {
|
||||
$res = $service->fetchMailbox($stabile, $mailbox, 20);
|
||||
$totImported += (int) ($res['imported'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -283,11 +364,17 @@ public function getMessagesProperty()
|
|||
: [];
|
||||
|
||||
$query = CommunicationMessage::with(['stabile', 'ticket'])
|
||||
->whereIn('stabile_id', $allowedStabileIds)
|
||||
->where('channel', 'pec');
|
||||
|
||||
if ($this->selectedStabileId) {
|
||||
$query->where('stabile_id', $this->selectedStabileId);
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$query->whereNull('stabile_id');
|
||||
} elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
$query->where('stabile_id', (int) $this->selectedStabileId);
|
||||
} else {
|
||||
$query->where(function ($q) use ($allowedStabileIds) {
|
||||
$q->whereIn('stabile_id', $allowedStabileIds)
|
||||
->orWhereNull('stabile_id');
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->currentFolder === 'sent') {
|
||||
|
|
@ -329,10 +416,17 @@ public function getFolderCountsProperty(): array
|
|||
? StabileContext::accessibleStabili($user)->pluck('id')->toArray()
|
||||
: [];
|
||||
|
||||
$base = CommunicationMessage::whereIn('stabile_id', $allowedStabileIds)->where('channel', 'pec');
|
||||
$base = CommunicationMessage::query()->where('channel', 'pec');
|
||||
|
||||
if ($this->selectedStabileId) {
|
||||
$base->where('stabile_id', $this->selectedStabileId);
|
||||
if ($this->selectedStabileId === 'studio') {
|
||||
$base->whereNull('stabile_id');
|
||||
} elseif (is_numeric($this->selectedStabileId) && (int) $this->selectedStabileId > 0) {
|
||||
$base->where('stabile_id', (int) $this->selectedStabileId);
|
||||
} else {
|
||||
$base->where(function ($q) use ($allowedStabileIds) {
|
||||
$q->whereIn('stabile_id', $allowedStabileIds)
|
||||
->orWhereNull('stabile_id');
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -1186,6 +1186,7 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
$codStabile = (string) $this->unita->stabile->codice_stabile;
|
||||
$scala = trim((string) $this->unita->scala);
|
||||
$interno = trim((string) $this->unita->interno);
|
||||
$condIds = $this->resolveLegacyCondIdsForUnita();
|
||||
|
||||
$results = [];
|
||||
|
||||
|
|
@ -1193,16 +1194,19 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
if (DbSchema::connection('gescon_import')->hasTable('protoc_ec')) {
|
||||
$ecQuery = DB::connection('gescon_import')->table('protoc_ec')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->where(function ($q) use ($scala, $interno) {
|
||||
->where(function ($q) use ($scala, $interno, $condIds) {
|
||||
$q->where(function ($sub) use ($scala, $interno) {
|
||||
$sub->where('scala', $scala)->where('interno', $interno);
|
||||
});
|
||||
if (! empty($condIds)) {
|
||||
$q->orWhereIn('id_condomino', $condIds);
|
||||
}
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
foreach ($ecQuery as $r) {
|
||||
$dtRaw = $r->data_invio ?? null;
|
||||
$dtRaw = $r->data_invio ?? ($r->data ?? null);
|
||||
$ts = 0;
|
||||
$dtFmt = '—';
|
||||
if ($dtRaw) {
|
||||
|
|
@ -1215,24 +1219,34 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
}
|
||||
}
|
||||
|
||||
$pdfName = trim((string) ($r->nome_pdf ?? ''));
|
||||
$pdfPath = $pdfName !== '' ? "/mnt/gescon-archives/gescon/{$codStabile}/E_C/{$pdfName}" : null;
|
||||
$pdfExists = $pdfPath && file_exists($pdfPath);
|
||||
$candidateNames = array_values(array_unique(array_filter([
|
||||
trim((string) ($r->nome_pdf ?? '')),
|
||||
trim((string) ($r->alleg_4 ?? '')),
|
||||
trim((string) ($r->lettera_tipo_caricata ?? '')),
|
||||
])));
|
||||
|
||||
$allegati = [];
|
||||
foreach ($candidateNames as $fn) {
|
||||
$allegati[] = \App\Services\Documenti\ArchiveFileResolver::resolve($codStabile, $fn, 'E_C');
|
||||
}
|
||||
|
||||
$firstAtt = $allegati[0] ?? null;
|
||||
|
||||
$results[] = [
|
||||
'id' => 'ec_' . $r->id,
|
||||
'protocollo' => $r->protocollo ?: ('EC #' . $r->id_corrisp),
|
||||
'protocollo' => $r->protocollo ?: ('EC #' . ($r->id_corrisp ?? $r->id)),
|
||||
'tipo' => 'Estratto Conto',
|
||||
'canale' => $r->tipo_documento ?: 'E-Mail',
|
||||
'data_invio' => $dtFmt,
|
||||
'destinatario' => $r->destinatario ?: $r->nome,
|
||||
'ruolo' => $r->c_i === 'I' ? 'Inquilino' : 'Condomino',
|
||||
'destinatario' => $r->destinatario ?: ($r->nome ?? '—'),
|
||||
'ruolo' => ($r->c_i ?? '') === 'I' ? 'Inquilino' : 'Condomino',
|
||||
'oggetto' => $r->oggetto ?: 'Invio Estratto Conto',
|
||||
'importo' => (float) ($r->totale ?? 0),
|
||||
'nome_file' => $pdfName,
|
||||
'file_path' => $pdfPath,
|
||||
'file_exists' => $pdfExists,
|
||||
'note' => $r->note,
|
||||
'allegati' => $allegati,
|
||||
'nome_file' => $firstAtt['filename'] ?? null,
|
||||
'file_path' => $firstAtt['path'] ?? null,
|
||||
'file_exists' => $firstAtt['exists'] ?? false,
|
||||
'note' => $r->note ?? null,
|
||||
'timestamp_sort' => $ts,
|
||||
];
|
||||
}
|
||||
|
|
@ -1240,7 +1254,6 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
|
||||
// 2. Dati da corrisp_inviata (Lettere, circolari)
|
||||
if (DbSchema::connection('gescon_import')->hasTable('corrisp_inviata')) {
|
||||
$condIds = $this->resolveLegacyCondIdsForUnita();
|
||||
$corrQuery = DB::connection('gescon_import')->table('corrisp_inviata')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->where(function ($q) use ($condIds) {
|
||||
|
|
@ -1253,7 +1266,7 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
->get();
|
||||
|
||||
foreach ($corrQuery as $r) {
|
||||
$dtRaw = $r->data_invio ?? null;
|
||||
$dtRaw = $r->data_invio ?? ($r->data ?? null);
|
||||
$ts = 0;
|
||||
$dtFmt = '—';
|
||||
if ($dtRaw) {
|
||||
|
|
@ -1266,30 +1279,112 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
}
|
||||
}
|
||||
|
||||
$docName = trim((string) ($r->lettera_tipo_caricata ?? ''));
|
||||
$docPath = $docName !== '' ? "/mnt/gescon-archives/gescon/{$codStabile}/{$docName}" : null;
|
||||
$docExists = $docPath && file_exists($docPath);
|
||||
$candidateNames = array_values(array_unique(array_filter([
|
||||
trim((string) ($r->lettera_tipo_caricata ?? '')),
|
||||
trim((string) ($r->nome_alleg_1 ?? '')),
|
||||
trim((string) ($r->nome_alleg_2 ?? '')),
|
||||
trim((string) ($r->nome_alleg_3 ?? '')),
|
||||
trim((string) ($r->nome_alleg_4 ?? '')),
|
||||
])));
|
||||
|
||||
$allegati = [];
|
||||
foreach ($candidateNames as $fn) {
|
||||
$allegati[] = \App\Services\Documenti\ArchiveFileResolver::resolve($codStabile, $fn, 'allegati');
|
||||
}
|
||||
|
||||
$firstAtt = $allegati[0] ?? null;
|
||||
|
||||
$results[] = [
|
||||
'id' => 'corr_' . $r->id,
|
||||
'protocollo' => $r->protocollo ?: ('PROT #' . $r->id_corrisp),
|
||||
'protocollo' => $r->protocollo ?: ('PROT #' . ($r->id_corrisp ?? $r->id)),
|
||||
'tipo' => 'Lettera / Circolare',
|
||||
'canale' => $r->tipo_documento ?: 'Posta',
|
||||
'data_invio' => $dtFmt,
|
||||
'destinatario' => $r->destinatario ?: 'Condomini',
|
||||
'destinatario' => $r->destinatario ?: ($r->nome ?? 'Condomini'),
|
||||
'ruolo' => 'Condomino',
|
||||
'oggetto' => $r->oggetto ?: 'Comunicazione Amministrazione',
|
||||
'importo' => null,
|
||||
'nome_file' => $docName,
|
||||
'file_path' => $docPath,
|
||||
'file_exists' => $docExists,
|
||||
'note' => $r->note,
|
||||
'allegati' => $allegati,
|
||||
'nome_file' => $firstAtt['filename'] ?? null,
|
||||
'file_path' => $firstAtt['path'] ?? null,
|
||||
'file_exists' => $firstAtt['exists'] ?? false,
|
||||
'note' => $r->note ?? null,
|
||||
'timestamp_sort' => $ts,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Dati da CommunicationMessage (Posta IMAP, PEC, EML)
|
||||
// 3. Dati da posta_dett (Raccomandate, visure, postalizzazione)
|
||||
if (DbSchema::connection('gescon_import')->hasTable('posta_dett')) {
|
||||
$postaQuery = DB::connection('gescon_import')->table('posta_dett')
|
||||
->where(function ($q) use ($scala, $interno, $condIds) {
|
||||
$hasCond = false;
|
||||
if ($scala !== '' && $interno !== '') {
|
||||
$q->where(function ($sub) use ($scala, $interno) {
|
||||
$sub->where('scala', $scala)->where('interno', $interno);
|
||||
});
|
||||
$hasCond = true;
|
||||
}
|
||||
if (! empty($condIds)) {
|
||||
if ($hasCond) {
|
||||
$q->orWhereIn('id_cond', $condIds);
|
||||
} else {
|
||||
$q->whereIn('id_cond', $condIds);
|
||||
}
|
||||
}
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
foreach ($postaQuery as $r) {
|
||||
$dtRaw = $r->ora_invio_richiesta ?? ($r->data ?? null);
|
||||
$ts = 0;
|
||||
$dtFmt = '—';
|
||||
if ($dtRaw) {
|
||||
try {
|
||||
$c = Carbon::parse((string) $dtRaw);
|
||||
$ts = $c->timestamp;
|
||||
$dtFmt = $c->format('d/m/Y');
|
||||
} catch (\Throwable $e) {
|
||||
$dtFmt = (string) $dtRaw;
|
||||
}
|
||||
}
|
||||
|
||||
$candidateNames = array_values(array_unique(array_filter([
|
||||
trim((string) ($r->nome_alleg_1 ?? '')),
|
||||
trim((string) ($r->nome_alleg_4 ?? '')),
|
||||
trim((string) ($r->documento ?? '')),
|
||||
trim((string) ($r->risposta ?? '')),
|
||||
])));
|
||||
|
||||
$allegati = [];
|
||||
foreach ($candidateNames as $fn) {
|
||||
$allegati[] = \App\Services\Documenti\ArchiveFileResolver::resolve($codStabile, $fn);
|
||||
}
|
||||
|
||||
$firstAtt = $allegati[0] ?? null;
|
||||
|
||||
$results[] = [
|
||||
'id' => 'posta_dett_' . $r->id,
|
||||
'protocollo' => $r->num_raccomandata ? ('RAC #' . $r->num_raccomandata) : ('POSTA #' . ($r->id_richiesta ?? $r->id)),
|
||||
'tipo' => $r->tipo_richiesta ?: 'Spedizione Postale / Visura',
|
||||
'canale' => 'Posta / Raccomandata',
|
||||
'data_invio' => $dtFmt,
|
||||
'destinatario' => $r->nome ?: ($r->destinatario ?? '—'),
|
||||
'ruolo' => ($r->c_i ?? '') === 'I' ? 'Inquilino' : 'Condomino',
|
||||
'oggetto' => $r->stato_richiesta ? ('Stato invio: ' . $r->stato_richiesta) : 'Invio Raccomandata / Documento',
|
||||
'importo' => (float) ($r->costo ?? ($r->importo_ec ?? 0)),
|
||||
'allegati' => $allegati,
|
||||
'nome_file' => $firstAtt['filename'] ?? null,
|
||||
'file_path' => $firstAtt['path'] ?? null,
|
||||
'file_exists' => $firstAtt['exists'] ?? false,
|
||||
'note' => $r->stato_richiesta ?? null,
|
||||
'timestamp_sort' => $ts,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Dati da CommunicationMessage (Posta IMAP, PEC, EML)
|
||||
if (DbSchema::hasTable('communication_messages')) {
|
||||
$unitEmails = [];
|
||||
$recapiti = $this->getRecapitiMulticanaleTableProperty();
|
||||
|
|
@ -1317,8 +1412,39 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
$dt = $cm->received_at ? Carbon::parse($cm->received_at) : null;
|
||||
$dtFmt = $dt ? $dt->format('d/m/Y H:i') : '—';
|
||||
$ts = $dt ? $dt->timestamp : 0;
|
||||
$attachments = is_array($cm->attachments) ? $cm->attachments : [];
|
||||
$firstAtt = $attachments[0] ?? null;
|
||||
$rawAttachments = is_array($cm->attachments) ? $cm->attachments : [];
|
||||
|
||||
$allegati = [];
|
||||
foreach ($rawAttachments as $att) {
|
||||
$fn = (string) ($att['filename'] ?? 'allegato');
|
||||
$fp = (string) ($att['path'] ?? '');
|
||||
$exists = $fp !== '' && file_exists($fp);
|
||||
$allegati[] = [
|
||||
'filename' => $fn,
|
||||
'path' => $fp,
|
||||
'exists' => $exists,
|
||||
'size' => (int) ($att['size'] ?? 0),
|
||||
'mime' => (string) ($att['content_type'] ?? 'application/octet-stream'),
|
||||
'is_pdf' => str_ends_with(strtolower($fn), '.pdf'),
|
||||
'is_image' => (bool) preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $fn),
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($allegati) && ! empty($meta['eml_path'])) {
|
||||
$emlPath = (string) $meta['eml_path'];
|
||||
$emlName = basename($emlPath);
|
||||
$allegati[] = [
|
||||
'filename' => $emlName,
|
||||
'path' => $emlPath,
|
||||
'exists' => file_exists($emlPath),
|
||||
'size' => file_exists($emlPath) ? (int) filesize($emlPath) : 0,
|
||||
'mime' => 'message/rfc822',
|
||||
'is_pdf' => false,
|
||||
'is_image' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$firstAtt = $allegati[0] ?? null;
|
||||
|
||||
$results[] = [
|
||||
'id' => 'comm_' . $cm->id,
|
||||
|
|
@ -1330,9 +1456,10 @@ public function getProtocolloComunicazioniProperty(): array
|
|||
'ruolo' => $cm->channel === 'pec' ? 'PEC' : 'Email',
|
||||
'oggetto' => $meta['subject'] ?? \Illuminate\Support\Str::limit($cm->message_text, 60),
|
||||
'importo' => null,
|
||||
'nome_file' => $firstAtt['filename'] ?? (! empty($meta['eml_path']) ? basename($meta['eml_path']) : null),
|
||||
'file_path' => $firstAtt['path'] ?? ($meta['eml_path'] ?? null),
|
||||
'file_exists' => ($firstAtt['path'] ?? null) ? file_exists($firstAtt['path']) : (! empty($meta['eml_path']) && file_exists($meta['eml_path'])),
|
||||
'allegati' => $allegati,
|
||||
'nome_file' => $firstAtt['filename'] ?? null,
|
||||
'file_path' => $firstAtt['path'] ?? null,
|
||||
'file_exists' => $firstAtt['exists'] ?? false,
|
||||
'note' => $cm->message_text ? \Illuminate\Support\Str::limit($cm->message_text, 120) : null,
|
||||
'timestamp_sort' => $ts,
|
||||
];
|
||||
|
|
|
|||
134
app/Services/Documenti/ArchiveFileResolver.php
Normal file
134
app/Services/Documenti/ArchiveFileResolver.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Documenti;
|
||||
|
||||
class ArchiveFileResolver
|
||||
{
|
||||
/**
|
||||
* Risolve il percorso assoluto di un file allegato o documento dello stabile,
|
||||
* effettuando una ricerca euristica nelle cartelle standard (E_C, INC_EC, WA, XML, allegati, posta, etc.).
|
||||
*
|
||||
* @param string|int $codStabile
|
||||
* @param string $filename
|
||||
* @param string|null $preferredSubdir
|
||||
* @return array{
|
||||
* filename: string,
|
||||
* path: ?string,
|
||||
* exists: bool,
|
||||
* size: int,
|
||||
* mime: string,
|
||||
* is_pdf: bool,
|
||||
* is_image: bool
|
||||
* }
|
||||
*/
|
||||
public static function resolve(string|int $codStabile, string $filename, ?string $preferredSubdir = null): array
|
||||
{
|
||||
$filename = trim($filename);
|
||||
if ($filename === '') {
|
||||
return [
|
||||
'filename' => '',
|
||||
'path' => null,
|
||||
'exists' => false,
|
||||
'size' => 0,
|
||||
'mime' => 'application/octet-stream',
|
||||
'is_pdf' => false,
|
||||
'is_image' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$cod = str_pad((string) $codStabile, 4, '0', STR_PAD_LEFT);
|
||||
$candidateSubdirs = [
|
||||
$preferredSubdir,
|
||||
'E_C', 'e_c',
|
||||
'INC_EC', 'inc_ec',
|
||||
'allegati', 'Allegati',
|
||||
'WA', 'wa',
|
||||
'XML', 'xml',
|
||||
'posta_ordinaria', 'posta_pec', 'posta',
|
||||
'documenti', 'Documenti',
|
||||
'',
|
||||
];
|
||||
$candidateSubdirs = array_values(array_unique(array_filter($candidateSubdirs, fn($v) => $v !== null)));
|
||||
|
||||
$baseRoots = [
|
||||
"/mnt/gescon-archives/gescon/{$cod}",
|
||||
"/mnt/gescon-archives/gescon/" . ltrim((string) $codStabile, '0'),
|
||||
storage_path("app/private/amministratori/ADMX3PD9/stabili/{$cod}"),
|
||||
storage_path("app/private/amministratori/ADMX3PD9/stabili/" . ltrim((string) $codStabile, '0')),
|
||||
storage_path("app/public/stabili/{$cod}"),
|
||||
storage_path("app/public/stabili/" . ltrim((string) $codStabile, '0')),
|
||||
];
|
||||
|
||||
$foundPath = null;
|
||||
|
||||
// 1. Cerca percorsi esatti
|
||||
foreach ($baseRoots as $root) {
|
||||
if (! is_dir($root)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($candidateSubdirs as $sub) {
|
||||
$dir = $sub !== '' ? "{$root}/{$sub}" : $root;
|
||||
$p = "{$dir}/{$filename}";
|
||||
if (file_exists($p)) {
|
||||
$foundPath = $p;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Se non trovato e il file ha un'estensione, cerca case-insensitive
|
||||
if (! $foundPath) {
|
||||
$lowerName = strtolower($filename);
|
||||
foreach ($baseRoots as $root) {
|
||||
if (! is_dir($root)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($candidateSubdirs as $sub) {
|
||||
$dir = $sub !== '' ? "{$root}/{$sub}" : $root;
|
||||
if (is_dir($dir)) {
|
||||
$files = @scandir($dir);
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $f) {
|
||||
if (strtolower($f) === $lowerName) {
|
||||
$foundPath = "{$dir}/{$f}";
|
||||
break 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$exists = $foundPath !== null && file_exists($foundPath);
|
||||
$size = $exists ? (int) @filesize($foundPath) : 0;
|
||||
$mime = 'application/octet-stream';
|
||||
$isPdf = str_ends_with(strtolower($filename), '.pdf');
|
||||
$isImg = (bool) preg_match('/\.(jpg|jpeg|png|webp|gif|bmp)$/i', $filename);
|
||||
|
||||
if ($exists) {
|
||||
$finfo = @finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo) {
|
||||
$detected = @finfo_file($finfo, $foundPath);
|
||||
if ($detected) {
|
||||
$mime = $detected;
|
||||
}
|
||||
@finfo_close($finfo);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isPdf) {
|
||||
$mime = 'application/pdf';
|
||||
}
|
||||
|
||||
return [
|
||||
'filename' => $filename,
|
||||
'path' => $foundPath,
|
||||
'exists' => $exists,
|
||||
'size' => $size,
|
||||
'mime' => $mime,
|
||||
'is_pdf' => $isPdf,
|
||||
'is_image' => $isImg,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,175 @@ public function fetchMailbox(Stabile $stabile, array $mailbox, int $maxMessages
|
|||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scarica e memorizza le email/PEC dello studio amministratore in formato .EML
|
||||
*
|
||||
* @param \App\Models\Amministratore $amministratore
|
||||
* @param array $mailbox
|
||||
* @param int $maxMessages
|
||||
* @return array{imported: int, skipped: int, errors: int, messages: array}
|
||||
*/
|
||||
public function fetchStudioMailbox(\App\Models\Amministratore $amministratore, array $mailbox, int $maxMessages = 50): array
|
||||
{
|
||||
$host = trim((string) ($mailbox['host'] ?? ''));
|
||||
$port = (int) ($mailbox['port'] ?? 993);
|
||||
$username = trim((string) ($mailbox['username'] ?? ($mailbox['email'] ?? '')));
|
||||
$password = (string) ($mailbox['password'] ?? '');
|
||||
$encryption = trim((string) ($mailbox['encryption'] ?? 'ssl')) ?: 'ssl';
|
||||
$folder = trim((string) ($mailbox['folder'] ?? 'INBOX')) ?: 'INBOX';
|
||||
$isPec = strtolower(trim((string) ($mailbox['tipo'] ?? ''))) === 'pec';
|
||||
|
||||
if ($host === '' || $username === '') {
|
||||
return [
|
||||
'imported' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 1,
|
||||
'message' => 'Parametri IMAP studio incompleti',
|
||||
'messages' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$stats = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'messages' => []];
|
||||
|
||||
try {
|
||||
$this->imapClient->connect($host, $port, $username, $password, $encryption);
|
||||
$this->imapClient->selectFolder($folder);
|
||||
$msgIds = $this->imapClient->search('ALL');
|
||||
|
||||
$recentIds = array_slice(array_reverse($msgIds), 0, $maxMessages);
|
||||
|
||||
foreach ($recentIds as $msgId) {
|
||||
try {
|
||||
$rawEml = $this->imapClient->fetchRawEml($msgId);
|
||||
if (trim($rawEml) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$res = $this->ingestStudioEmlString($rawEml, $amministratore, $mailbox, $isPec);
|
||||
if ($res['status'] === 'imported') {
|
||||
$stats['imported']++;
|
||||
$stats['messages'][] = $res;
|
||||
} elseif ($res['status'] === 'duplicate') {
|
||||
$stats['skipped']++;
|
||||
} else {
|
||||
$stats['errors']++;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$stats['errors']++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->imapClient->disconnect();
|
||||
} catch (\Throwable $e) {
|
||||
$stats['errors']++;
|
||||
$stats['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingesta un messaggio EML dello studio amministratore
|
||||
*/
|
||||
public function ingestStudioEmlString(
|
||||
string $rawEml,
|
||||
\App\Models\Amministratore $amministratore,
|
||||
array $mailboxConfig = [],
|
||||
bool $forcePec = false
|
||||
): array {
|
||||
$parsed = $this->emlParser->parse($rawEml);
|
||||
$messageId = $parsed['message_id'] ?: ('studio_' . md5($rawEml));
|
||||
|
||||
$existing = CommunicationMessage::whereNull('stabile_id')
|
||||
->where(function ($q) use ($messageId) {
|
||||
$q->where('external_message_id', $messageId)
|
||||
->orWhere('metadata->message_id', $messageId);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return ['status' => 'duplicate', 'id' => $existing->id];
|
||||
}
|
||||
|
||||
$channel = ($forcePec || $parsed['is_pec']) ? 'pec' : 'email';
|
||||
$year = $parsed['date'] ? $parsed['date']->format('Y') : date('Y');
|
||||
|
||||
$postaSubdir = $channel === 'pec' ? 'posta_pec' : 'posta_ordinaria';
|
||||
$storageDir = $this->pathService->amministratoreAbsolutePath($amministratore, "studio/{$postaSubdir}/{$year}");
|
||||
if (! is_dir($storageDir)) {
|
||||
@mkdir($storageDir, 0755, true);
|
||||
}
|
||||
|
||||
$safeMsgId = preg_replace('/[^a-zA-Z0-9_-]/', '_', $messageId);
|
||||
$emlFilename = "{$safeMsgId}.eml";
|
||||
$emlFullPath = "{$storageDir}/{$emlFilename}";
|
||||
@file_put_contents($emlFullPath, $rawEml);
|
||||
|
||||
$savedAttachments = [];
|
||||
if (! empty($parsed['attachments'])) {
|
||||
$attachmentsDir = "{$storageDir}/allegati/{$safeMsgId}";
|
||||
if (! is_dir($attachmentsDir)) {
|
||||
@mkdir($attachmentsDir, 0755, true);
|
||||
}
|
||||
|
||||
foreach ($parsed['attachments'] as $idx => $att) {
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', (string) $att['filename']);
|
||||
if ($safeName === '') {
|
||||
$safeName = "allegato_{$idx}.dat";
|
||||
}
|
||||
$attPath = "{$attachmentsDir}/{$safeName}";
|
||||
@file_put_contents($attPath, $att['content']);
|
||||
|
||||
$savedAttachments[] = [
|
||||
'filename' => $att['filename'],
|
||||
'path' => $attPath,
|
||||
'content_type' => $att['content_type'],
|
||||
'size' => $att['size'],
|
||||
'is_inline' => $att['is_inline'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$senderDisplay = $parsed['from']['name'] ? "{$parsed['from']['name']} <{$parsed['from']['email']}>" : $parsed['from']['email'];
|
||||
|
||||
$comm = CommunicationMessage::create([
|
||||
'channel' => $channel,
|
||||
'direction' => 'inbound',
|
||||
'external_message_id' => $messageId,
|
||||
'stabile_id' => null,
|
||||
'sender_name' => $senderDisplay,
|
||||
'phone_number' => null,
|
||||
'message_text' => $parsed['body_text'] ?: strip_tags((string) $parsed['body_html']),
|
||||
'attachments' => $savedAttachments,
|
||||
'ticket_id' => null,
|
||||
'status' => 'received',
|
||||
'received_at' => $parsed['date'] ?: now(),
|
||||
'metadata' => [
|
||||
'subject' => $parsed['subject'],
|
||||
'sender_email' => $parsed['from']['email'],
|
||||
'sender_name' => $parsed['from']['name'],
|
||||
'to' => $parsed['to'],
|
||||
'cc' => $parsed['cc'],
|
||||
'body_html' => $parsed['body_html'],
|
||||
'eml_path' => $emlFullPath,
|
||||
'is_pec' => $channel === 'pec',
|
||||
'is_studio' => true,
|
||||
'mailbox_label' => $mailboxConfig['label'] ?? 'Studio',
|
||||
'mailbox_email' => $mailboxConfig['email'] ?? '',
|
||||
],
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'imported',
|
||||
'id' => $comm->id,
|
||||
'channel' => $channel,
|
||||
'subject' => $parsed['subject'],
|
||||
'from' => $senderDisplay,
|
||||
'date' => $parsed['date']?->format('d/m/Y H:i'),
|
||||
'has_attachments' => count($savedAttachments) > 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingesta un messaggio da stringa EML grezza
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,18 +1,202 @@
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-3 text-xs text-gray-700">
|
||||
Questa area serve a verificare via web la prontezza dell'integrazione Google e della posta studio. La lettura automatica caselle non e ancora attiva come daemon applicativo, ma puoi validare configurazione OAuth e collegamento account senza entrare in shell.
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
{{-- Google OAuth & General Studio Actions --}}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-bold text-sm text-slate-900 flex items-center gap-2">
|
||||
<span>🌐</span>
|
||||
<span>Integrazione Google Workspace & Posta Studio</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-500">Convalida rapida OAuth e sincronizzazione IMAP</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::button type="button" color="success" wire:click="connectGoogle">Collega Google</x-filament::button>
|
||||
<x-filament::button type="button" color="info" wire:click="runGoogleOAuthReadinessCheck">Verifica Google OAuth</x-filament::button>
|
||||
<x-filament::button type="button" color="danger" wire:click="disconnectGoogle">Scollega Google</x-filament::button>
|
||||
<div class="text-xs text-slate-600 leading-relaxed">
|
||||
Verifica via web la prontezza dell'integrazione Google e delle caselle IMAP/PEC dello studio. Le credenziali salvate sopra vengono utilizzate per scaricare le comunicazioni in formato canonico <code class="bg-slate-100 px-1 py-0.5 rounded text-indigo-600 font-mono">.EML</code>.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 pt-1">
|
||||
<x-filament::button type="button" color="success" wire:click="connectGoogle">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span>🔑</span>
|
||||
<span>Collega Account Google</span>
|
||||
</span>
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="info" wire:click="runGoogleOAuthReadinessCheck">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span>🔍</span>
|
||||
<span>Verifica Google OAuth</span>
|
||||
</span>
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="danger" wire:click="disconnectGoogle">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span>✕</span>
|
||||
<span>Scollega Google</span>
|
||||
</span>
|
||||
</x-filament::button>
|
||||
|
||||
<a
|
||||
href="{{ url('/admin-filament/comunicazioni/posta-ordinaria?stabile_id=studio') }}"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 text-blue-700 hover:bg-blue-100 border border-blue-200 text-xs font-semibold rounded-lg shadow-xs transition"
|
||||
>
|
||||
<span>✉️</span>
|
||||
<span>Webmail Ordinaria Studio</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{ url('/admin-filament/comunicazioni/posta-pec?stabile_id=studio') }}"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 border border-emerald-200 text-xs font-semibold rounded-lg shadow-xs transition"
|
||||
>
|
||||
<span>🛡️</span>
|
||||
<span>Webmail PEC Studio</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(filled($this->opsLastOutput))
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-700">Ultimo output verifica posta / Google</div>
|
||||
<pre class="max-h-64 overflow-auto whitespace-pre-wrap text-xs text-gray-800">{{ $this->opsLastOutput }}</pre>
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4 shadow-sm">
|
||||
<div class="mb-2 text-xs font-bold text-slate-700 uppercase tracking-wider">Ultimo output verifica posta / Google</div>
|
||||
<pre class="max-h-64 overflow-auto whitespace-pre-wrap text-xs font-mono text-slate-800 bg-white p-3 rounded-xl border border-slate-200">{{ $this->opsLastOutput }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Stabili Mailboxes Overview Table (All 16 Stabili) --}}
|
||||
@php
|
||||
$stabiliStatus = $this->stabiliMailboxStatus;
|
||||
@endphp
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden space-y-0">
|
||||
<div class="border-b border-slate-200 bg-slate-50/80 px-4 py-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="text-lg">📬</span>
|
||||
<div>
|
||||
<div class="font-bold text-sm text-slate-900">Stato Posta e PEC degli Stabili Gestiti</div>
|
||||
<div class="text-xs text-slate-500">Quadro di configurazione e monitoraggio messaggi per tutti i {{ count($stabiliStatus) }} stabili</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full bg-indigo-50 border border-indigo-200 px-3 py-1 text-xs font-bold text-indigo-800">
|
||||
{{ count($stabiliStatus) }} Stabili Attivi
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100/70 text-slate-700 font-semibold border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Stabile</th>
|
||||
<th class="px-4 py-3">Codice Fiscale</th>
|
||||
<th class="px-4 py-3">Posta Ordinaria (IMAP)</th>
|
||||
<th class="px-4 py-3">Posta Certificata (PEC)</th>
|
||||
<th class="px-4 py-3 text-center">Messaggi</th>
|
||||
<th class="px-4 py-3 text-right">Azioni Rapide</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($stabiliStatus as $stb)
|
||||
<tr class="hover:bg-slate-50/60 transition">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-bold text-slate-900 text-sm flex items-center gap-1.5">
|
||||
<span class="px-1.5 py-0.5 rounded bg-slate-200 text-slate-800 font-mono text-[11px]">{{ $stb['codice_stabile'] }}</span>
|
||||
<span>{{ $stb['denominazione'] }}</span>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500 mt-0.5">{{ $stb['comune'] ?: '—' }}</div>
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 font-mono text-[11px] text-slate-700">
|
||||
{{ $stb['codice_fiscale'] ?: '—' }}
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3">
|
||||
@if($stb['email_configured'])
|
||||
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-800 font-semibold text-[11px]">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
<span>Configurata</span>
|
||||
</div>
|
||||
@if($stb['email_info'])
|
||||
<div class="text-[10px] text-slate-400 font-mono truncate max-w-xs mt-1" title="{{ $stb['email_info'] }}">
|
||||
{{ $stb['email_info'] }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-slate-100 border border-slate-200 text-slate-500 font-medium text-[11px]">
|
||||
<span class="w-2 h-2 rounded-full bg-slate-400"></span>
|
||||
<span>Non configurata</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3">
|
||||
@if($stb['pec_configured'])
|
||||
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-800 font-semibold text-[11px]">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
<span>Configurata</span>
|
||||
</div>
|
||||
@if($stb['pec_info'])
|
||||
<div class="text-[10px] text-slate-400 font-mono truncate max-w-xs mt-1" title="{{ $stb['pec_info'] }}">
|
||||
{{ $stb['pec_info'] }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-slate-100 border border-slate-200 text-slate-500 font-medium text-[11px]">
|
||||
<span class="w-2 h-2 rounded-full bg-slate-400"></span>
|
||||
<span>Non configurata</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($stb['msg_count'] > 0)
|
||||
<span class="px-2 py-0.5 rounded-full bg-blue-100 text-blue-800 font-bold font-mono text-xs">
|
||||
{{ $stb['msg_count'] }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-slate-400 font-mono text-xs">0</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<a
|
||||
href="{{ $stb['config_url'] }}"
|
||||
class="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-200 transition"
|
||||
title="Configura casella IMAP/PEC dello stabile"
|
||||
>
|
||||
<x-heroicon-m-cog-6-tooth class="w-3.5 h-3.5 text-slate-500" />
|
||||
<span>Configura</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{ $stb['webmail_email_url'] }}"
|
||||
class="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold rounded-lg bg-blue-50 hover:bg-blue-100 text-blue-700 border border-blue-200 transition"
|
||||
title="Apri Webmail Posta Ordinaria"
|
||||
>
|
||||
<span>✉️</span>
|
||||
<span>Posta</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{ $stb['webmail_pec_url'] }}"
|
||||
class="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold rounded-lg bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 transition"
|
||||
title="Apri Webmail PEC"
|
||||
>
|
||||
<span>🛡️</span>
|
||||
<span>PEC</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-slate-400">
|
||||
Nessuno stabile accessibile censito.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -39,6 +39,15 @@ class="w-full py-2 px-3 text-xs rounded-xl bg-gray-50 dark:bg-gray-900 border bo
|
|||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
<a
|
||||
href="{{ $this->configMailboxUrl }}"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 text-xs font-semibold rounded-xl shadow-xs transition"
|
||||
title="Configura parametri IMAP e credenziali della casella"
|
||||
>
|
||||
<x-heroicon-m-cog-6-tooth class="w-4 h-4 text-gray-500 dark:text-gray-300" />
|
||||
<span>Configura IMAP</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
wire:click="syncNow"
|
||||
wire:loading.attr="disabled"
|
||||
|
|
@ -427,12 +436,28 @@ class="text-gray-400 hover:text-amber-500 transition"
|
|||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="p-12 text-center text-gray-500 dark:text-gray-400 space-y-2">
|
||||
<div class="p-12 text-center text-gray-500 dark:text-gray-400 space-y-3">
|
||||
<x-heroicon-o-envelope-open class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" />
|
||||
<div class="font-medium text-base">Nessun messaggio presente in questa cartella</div>
|
||||
<p class="text-xs text-gray-400 max-w-sm mx-auto">
|
||||
Usa il pulsante "Sincronizza IMAP (.EML)" in alto per scaricare la posta dalle caselle ufficiali degli stabili.
|
||||
<div class="font-medium text-base text-gray-800 dark:text-gray-200">Nessun messaggio presente in questa cartella</div>
|
||||
<p class="text-xs text-gray-400 max-w-md mx-auto">
|
||||
Se non hai ancora configurato la casella IMAP o PEC, puoi inserire parametri e credenziali per abilitare lo scaricamento e la visualizzazione automatica.
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 pt-2">
|
||||
<a
|
||||
href="{{ $this->configMailboxUrl }}"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-2 bg-primary-600 hover:bg-primary-700 text-white text-xs font-semibold rounded-xl shadow-sm transition"
|
||||
>
|
||||
<x-heroicon-m-cog-6-tooth class="w-4 h-4" />
|
||||
<span>Configura Casella Stabile</span>
|
||||
</a>
|
||||
<a
|
||||
href="{{ url('/admin-filament/impostazioni/scheda-amministratore?tab=posta-operativita') }}"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 text-xs font-semibold rounded-xl shadow-xs transition"
|
||||
>
|
||||
<x-heroicon-m-building-office-2 class="w-4 h-4" />
|
||||
<span>Configura Caselle Studio</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,132 +1,288 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ $this->getTicketInserimentoUrl() }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Vai a Inserimento Ticket</a>
|
||||
<div class="space-y-6">
|
||||
{{-- KPI Summary Stats Header --}}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div class="rounded-2xl border border-blue-100 dark:border-blue-900/50 bg-gradient-to-br from-blue-50/80 to-white dark:from-blue-950/40 dark:to-gray-800 p-4 shadow-xs">
|
||||
<div class="flex items-center justify-between text-blue-600 dark:text-blue-400">
|
||||
<span class="text-xs font-bold uppercase tracking-wider">Aperti</span>
|
||||
<span class="p-1.5 rounded-xl bg-blue-100/80 dark:bg-blue-900/50">🎟️</span>
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-bold text-gray-900 dark:text-white font-mono">{{ $ticketCounters['open'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-[11px] text-gray-500">Ticket in attesa o assegnati</div>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Aperti: <span class="font-semibold">{{ $ticketCounters['open'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Urgenti: <span class="font-semibold">{{ $ticketCounters['urgent'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Chiusi: <span class="font-semibold">{{ $ticketCounters['closed'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Totali: <span class="font-semibold">{{ $ticketCounters['all'] ?? 0 }}</span></div>
|
||||
|
||||
<div class="rounded-2xl border border-rose-100 dark:border-rose-900/50 bg-gradient-to-br from-rose-50/80 to-white dark:from-rose-950/40 dark:to-gray-800 p-4 shadow-xs">
|
||||
<div class="flex items-center justify-between text-rose-600 dark:text-rose-400">
|
||||
<span class="text-xs font-bold uppercase tracking-wider">Urgenti</span>
|
||||
<span class="p-1.5 rounded-xl bg-rose-100/80 dark:bg-rose-900/50">🚨</span>
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-bold text-rose-600 dark:text-rose-400 font-mono">{{ $ticketCounters['urgent'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-[11px] text-gray-500">Priorità alta o emergenza</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-emerald-100 dark:border-emerald-900/50 bg-gradient-to-br from-emerald-50/80 to-white dark:from-emerald-950/40 dark:to-gray-800 p-4 shadow-xs">
|
||||
<div class="flex items-center justify-between text-emerald-600 dark:text-emerald-400">
|
||||
<span class="text-xs font-bold uppercase tracking-wider">Chiusi</span>
|
||||
<span class="p-1.5 rounded-xl bg-emerald-100/80 dark:bg-emerald-900/50">✅</span>
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-bold text-gray-900 dark:text-white font-mono">{{ $ticketCounters['closed'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-[11px] text-gray-500">Interventi completati e risolti</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-xs">
|
||||
<div class="flex items-center justify-between text-slate-600 dark:text-gray-400">
|
||||
<span class="text-xs font-bold uppercase tracking-wider">Totali</span>
|
||||
<span class="p-1.5 rounded-xl bg-slate-100 dark:bg-gray-700">📊</span>
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-bold text-gray-900 dark:text-white font-mono">{{ $ticketCounters['all'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-[11px] text-gray-500">Volume storico complessivo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button" wire:click="apriElenco" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'elenco' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
Tab 1 - Elenco (stile excel)
|
||||
{{-- Main Content Container --}}
|
||||
<div class="rounded-2xl border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm space-y-4">
|
||||
{{-- Navigation Bar & Filter --}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b border-gray-100 dark:border-gray-700">
|
||||
<div class="flex flex-wrap items-center gap-1.5 bg-gray-100 dark:bg-gray-900/60 p-1 rounded-xl">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="apriElenco"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition {{ $activeTab === 'elenco' ? 'bg-white dark:bg-gray-800 text-primary-700 dark:text-primary-300 shadow-xs' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}"
|
||||
>
|
||||
<span>📋</span>
|
||||
<span>Elenco Ticket</span>
|
||||
</button>
|
||||
<button type="button" wire:click="$set('activeTab', 'scheda')" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'scheda' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
Tab 2 - Scheda ticket
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('activeTab', 'scheda')"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition {{ $activeTab === 'scheda' ? 'bg-white dark:bg-gray-800 text-primary-700 dark:text-primary-300 shadow-xs' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}"
|
||||
>
|
||||
<span>📑</span>
|
||||
<span>Scheda Dettaglio</span>
|
||||
</button>
|
||||
<button type="button" wire:click="$set('activeTab', 'fornitori')" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'fornitori' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
Tab 3 - Fornitori attivi
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('activeTab', 'fornitori')"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition {{ $activeTab === 'fornitori' ? 'bg-white dark:bg-gray-800 text-primary-700 dark:text-primary-300 shadow-xs' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}"
|
||||
>
|
||||
<span>👷</span>
|
||||
<span>Fornitori Attivi</span>
|
||||
</button>
|
||||
<button type="button" wire:click="apriAssicurazioneTicket" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'assicurazione' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
Tab 4 - Assicurazioni
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="apriAssicurazioneTicket"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition {{ $activeTab === 'assicurazione' ? 'bg-white dark:bg-gray-800 text-primary-700 dark:text-primary-300 shadow-xs' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}"
|
||||
>
|
||||
<span>🛡️</span>
|
||||
<span>Sinistri & Assicurazioni</span>
|
||||
</button>
|
||||
</div>
|
||||
@if($activeTab === 'elenco')
|
||||
<select wire:model.live="status" class="rounded-lg border-gray-300 text-sm">
|
||||
<option value="open">Aperti</option>
|
||||
<option value="urgent">Urgenti</option>
|
||||
<option value="closed">Chiusi</option>
|
||||
<option value="all">Tutti</option>
|
||||
</select>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@if($activeTab === 'elenco')
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500 font-medium">Stato:</span>
|
||||
<select wire:model.live="status" class="py-1.5 px-3 text-xs font-semibold rounded-xl bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 text-gray-800 dark:text-gray-200">
|
||||
<option value="open">Aperti (Tutti)</option>
|
||||
<option value="urgent">Solo Urgenti</option>
|
||||
<option value="closed">Chiusi</option>
|
||||
<option value="all">Tutti gli stati</option>
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<a
|
||||
href="{{ $this->getTicketInserimentoUrl() }}"
|
||||
class="inline-flex items-center gap-1.5 rounded-xl bg-primary-600 hover:bg-primary-700 px-3.5 py-1.5 text-xs font-semibold text-white shadow-xs transition"
|
||||
>
|
||||
<x-heroicon-m-plus class="w-4 h-4" />
|
||||
<span>Nuovo Ticket</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($activeTab === 'elenco')
|
||||
<div class="mt-3 rounded-lg border bg-slate-50 p-3">
|
||||
<div class="mb-2 text-xs font-semibold text-slate-700">Categorie ticket</div>
|
||||
<div class="grid gap-2 md:grid-cols-3">
|
||||
<label class="block text-xs">
|
||||
<span class="mb-1 block font-medium">Categoria esistente</span>
|
||||
<select wire:model.live="selectedCategoriaId" class="w-full rounded-lg border-gray-300 text-sm">
|
||||
<option value="">Seleziona categoria</option>
|
||||
{{-- Categories Quick Management Accordion/Panel --}}
|
||||
<div class="rounded-xl border border-slate-200 dark:border-gray-700 bg-slate-50/60 dark:bg-gray-900/30 p-3 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-gray-300 flex items-center gap-1.5">
|
||||
<span>🏷️</span>
|
||||
<span>Gestione Categorie Ticket</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-3 items-end">
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-gray-600 dark:text-gray-400 mb-1">Categoria Esistente</label>
|
||||
<select wire:model.live="selectedCategoriaId" class="w-full py-1.5 px-2.5 rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-xs text-gray-800 dark:text-gray-200">
|
||||
<option value="">-- Seleziona per modificare --</option>
|
||||
@foreach($categorieOptions as $cat)
|
||||
<option value="{{ (int) $cat['id'] }}">{{ $cat['nome'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-xs">
|
||||
<span class="mb-1 block font-medium">Nome categoria</span>
|
||||
<input type="text" wire:model.defer="categoriaNome" class="w-full rounded-lg border-gray-300 text-sm" placeholder="Es. Ascensore" />
|
||||
</label>
|
||||
<label class="block text-xs">
|
||||
<span class="mb-1 block font-medium">Descrizione</span>
|
||||
<input type="text" wire:model.defer="categoriaDescrizione" class="w-full rounded-lg border-gray-300 text-sm" placeholder="Descrizione breve" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-gray-600 dark:text-gray-400 mb-1">Nome Categoria</label>
|
||||
<input type="text" wire:model.defer="categoriaNome" class="w-full py-1.5 px-2.5 rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-xs text-gray-800 dark:text-gray-200" placeholder="Es. Ascensore, Autoclave..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-gray-600 dark:text-gray-400 mb-1">Descrizione</label>
|
||||
<input type="text" wire:model.defer="categoriaDescrizione" class="w-full py-1.5 px-2.5 rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-xs text-gray-800 dark:text-gray-200" placeholder="Breve descrizione opzionale" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||
<x-filament::button size="sm" color="success" wire:click="creaCategoriaTicket">Aggiungi categoria</x-filament::button>
|
||||
<x-filament::button size="sm" color="warning" wire:click="aggiornaCategoriaTicket">Modifica categoria selezionata</x-filament::button>
|
||||
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<button type="button" wire:click="creaCategoriaTicket" class="px-3 py-1 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-semibold shadow-xs transition">
|
||||
+ Aggiungi Categoria
|
||||
</button>
|
||||
@if($selectedCategoriaId)
|
||||
<button type="button" wire:click="aggiornaCategoriaTicket" class="px-3 py-1 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-xs font-semibold shadow-xs transition">
|
||||
Modifica Selezionata
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 overflow-x-auto">
|
||||
<table class="min-w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700">
|
||||
<th class="border px-2 py-2 text-left">ID</th>
|
||||
<th class="border px-2 py-2 text-left">Titolo</th>
|
||||
<th class="border px-2 py-2 text-left">Categoria</th>
|
||||
<th class="border px-2 py-2 text-left">Tipo intervento</th>
|
||||
<th class="border px-2 py-2 text-left">Assegnato a</th>
|
||||
<th class="border px-2 py-2 text-left">Priorita</th>
|
||||
<th class="border px-2 py-2 text-left">Stato</th>
|
||||
<th class="border px-2 py-2 text-left">Apertura</th>
|
||||
<th class="border px-2 py-2 text-left">Azioni</th>
|
||||
{{-- Table View --}}
|
||||
<div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-gray-700">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100/80 dark:bg-gray-900 text-slate-700 dark:text-gray-300 font-semibold border-b border-slate-200 dark:border-gray-700">
|
||||
<tr>
|
||||
<th class="px-3 py-2.5">ID</th>
|
||||
<th class="px-3 py-2.5">Titolo & Allegati</th>
|
||||
<th class="px-3 py-2.5">Categoria</th>
|
||||
<th class="px-3 py-2.5">Assegnato a</th>
|
||||
<th class="px-3 py-2.5 text-center">Priorità</th>
|
||||
<th class="px-3 py-2.5 text-center">Stato</th>
|
||||
<th class="px-3 py-2.5">Data Apertura</th>
|
||||
<th class="px-3 py-2.5 text-right">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-gray-700">
|
||||
@forelse($tickets as $ticket)
|
||||
<tr id="ticket-{{ (int) $ticket->id }}" class="hover:bg-slate-50">
|
||||
<td class="border px-2 py-2">#{{ (int) $ticket->id }}</td>
|
||||
<td class="border px-2 py-2">
|
||||
<div class="flex items-center gap-1">
|
||||
@php
|
||||
$prio = strtolower((string) $ticket->priorita);
|
||||
$prioBadge = match($prio) {
|
||||
'urgente', 'emergenza' => 'bg-rose-100 text-rose-800 dark:bg-rose-950/60 dark:text-rose-300 border-rose-200',
|
||||
'alta' => 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border-amber-200',
|
||||
'media' => 'bg-sky-100 text-sky-800 dark:bg-sky-950/60 dark:text-sky-300 border-sky-200',
|
||||
default => 'bg-slate-100 text-slate-700 dark:bg-gray-800 dark:text-gray-300 border-slate-200',
|
||||
};
|
||||
|
||||
$stato = strtolower((string) $ticket->stato);
|
||||
$statoBadge = match($stato) {
|
||||
'aperto' => 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300 border-blue-200',
|
||||
'preso in carico', 'in lavorazione' => 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border-amber-200',
|
||||
'risolto', 'completato' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300 border-emerald-200',
|
||||
'chiuso' => 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400 border-gray-300',
|
||||
default => 'bg-slate-100 text-slate-800 border-slate-200',
|
||||
};
|
||||
@endphp
|
||||
<tr id="ticket-{{ (int) $ticket->id }}" class="hover:bg-slate-50/60 dark:hover:bg-gray-700/40 transition">
|
||||
<td class="px-3 py-2.5 font-mono font-bold text-slate-900 dark:text-gray-100">
|
||||
#{{ (int) $ticket->id }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2.5">
|
||||
<div class="font-semibold text-slate-900 dark:text-gray-100 flex items-center gap-1.5">
|
||||
<span>{{ $ticket->titolo }}</span>
|
||||
@if(((int) ($ticket->attachments_count ?? 0)) > 0)
|
||||
<span title="Presenza allegati" class="inline-flex items-center rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-800">
|
||||
Allegati {{ (int) $ticket->attachments_count }}
|
||||
<span class="inline-flex items-center gap-0.5 rounded-full bg-purple-50 text-purple-700 dark:bg-purple-950/60 dark:text-purple-300 border border-purple-200 px-1.5 py-0.2 text-[10px] font-bold" title="Presenza allegati">
|
||||
📎 {{ (int) $ticket->attachments_count }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($ticket->stabile)
|
||||
<div class="text-[10px] text-slate-500 font-mono mt-0.5">
|
||||
{{ $ticket->stabile->codice_stabile }} - {{ $ticket->stabile->denominazione }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="border px-2 py-2">{{ optional($ticket->categoriaTicket)->nome ?: '-' }}</td>
|
||||
<td class="border px-2 py-2">{{ $this->getTipoInterventoLabel($ticket) }}</td>
|
||||
<td class="border px-2 py-2">{{ optional($ticket->assegnatoAUser)->name ?: '-' }}</td>
|
||||
<td class="border px-2 py-2">{{ $ticket->priorita }}</td>
|
||||
<td class="border px-2 py-2">{{ $ticket->stato }}</td>
|
||||
<td class="border px-2 py-2">{{ optional($ticket->data_apertura)->format('d/m/Y H:i') }}</td>
|
||||
<td class="border px-2 py-2">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<button type="button" wire:click="apriScheda({{ (int) $ticket->id }})" class="inline-flex items-center rounded-md bg-slate-700 px-2 py-1 text-[11px] font-medium text-white hover:bg-slate-600">Scheda</button>
|
||||
|
||||
@if((int) ($ticket->assegnato_a_fornitore_id ?? 0) > 0)
|
||||
<button type="button" wire:click="apriScheda({{ (int) $ticket->id }})" class="inline-flex items-center rounded-md bg-fuchsia-700 px-2 py-1 text-[11px] font-medium text-white hover:bg-fuchsia-600">Assegnata</button>
|
||||
@endif
|
||||
<td class="px-3 py-2.5 text-slate-700 dark:text-gray-300">
|
||||
{{ optional($ticket->categoriaTicket)->nome ?: '—' }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2.5 text-slate-700 dark:text-gray-300">
|
||||
{{ optional($ticket->assegnatoAUser)->name ?: (optional($ticket->assegnatoAFornitore)->ragione_sociale ?: '—') }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2.5 text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border {{ $prioBadge }}">
|
||||
{{ $ticket->priorita }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2.5 text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border {{ $statoBadge }}">
|
||||
{{ $ticket->stato }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2.5 text-slate-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{{ optional($ticket->data_apertura)->format('d/m/Y H:i') ?: '—' }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2.5 text-right whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="apriScheda({{ (int) $ticket->id }})"
|
||||
class="px-2 py-1 text-[11px] font-semibold rounded-lg bg-slate-800 hover:bg-slate-700 text-white transition"
|
||||
>
|
||||
Scheda
|
||||
</button>
|
||||
|
||||
@if(in_array($ticket->stato, ['Aperto'], true))
|
||||
<button type="button" wire:click="prendiInCarico({{ (int) $ticket->id }})" class="inline-flex items-center rounded-md bg-blue-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-blue-500">Carico</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="prendiInCarico({{ (int) $ticket->id }})"
|
||||
class="px-2 py-1 text-[11px] font-semibold rounded-lg bg-blue-600 hover:bg-blue-700 text-white transition"
|
||||
>
|
||||
Carico
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if(in_array($ticket->stato, ['Aperto', 'Preso in Carico'], true))
|
||||
<button type="button" wire:click="avviaLavorazione({{ (int) $ticket->id }})" class="inline-flex items-center rounded-md bg-amber-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-amber-500">Lavora</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="avviaLavorazione({{ (int) $ticket->id }})"
|
||||
class="px-2 py-1 text-[11px] font-semibold rounded-lg bg-amber-600 hover:bg-amber-700 text-white transition"
|
||||
>
|
||||
Lavora
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if(in_array($ticket->stato, ['Aperto', 'Preso in Carico', 'In Lavorazione', 'In Attesa Approvazione', 'In Attesa Ricambi'], true))
|
||||
<button type="button" wire:click="risolviTicket({{ (int) $ticket->id }})" class="inline-flex items-center rounded-md bg-emerald-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-emerald-500">Risolto</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="risolviTicket({{ (int) $ticket->id }})"
|
||||
class="px-2 py-1 text-[11px] font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white transition"
|
||||
>
|
||||
Risolvi
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if(in_array($ticket->stato, ['Risolto'], true))
|
||||
<button type="button" wire:click="chiudiTicket({{ (int) $ticket->id }})" class="inline-flex items-center rounded-md bg-gray-800 px-2 py-1 text-[11px] font-medium text-white hover:bg-gray-700">Chiudi</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="chiudiTicket({{ (int) $ticket->id }})"
|
||||
class="px-2 py-1 text-[11px] font-semibold rounded-lg bg-gray-700 hover:bg-gray-600 text-white transition"
|
||||
>
|
||||
Chiudi
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="border px-2 py-4 text-center text-gray-500">Nessun ticket per il filtro selezionato.</td>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-slate-400">
|
||||
Nessun ticket presente per il filtro selezionato.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -1119,17 +1119,31 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-xs font-semibol
|
|||
<td class="px-4 py-2.5 text-right font-mono font-bold {{ ($prot['importo'] ?? 0) > 0 ? 'text-slate-900' : 'text-slate-400' }}">
|
||||
{{ $prot['importo'] !== null ? '€ ' . number_format($prot['importo'], 2, ',', '.') : '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-center whitespace-nowrap">
|
||||
@if(!empty($prot['nome_file']))
|
||||
<button
|
||||
type="button"
|
||||
wire:click="previewProtocolDoc('{{ $prot['id'] }}', '{{ addslashes($prot['oggetto'] ?? 'Documento') }}', '{{ addslashes($prot['file_path'] ?? '') }}')"
|
||||
class="inline-flex items-center gap-1 rounded bg-slate-100 hover:bg-indigo-100 hover:text-indigo-800 border border-slate-200 px-2 py-0.5 text-[11px] font-mono text-slate-700 transition cursor-pointer"
|
||||
title="Visualizza anteprima documento"
|
||||
>
|
||||
<span>📄</span>
|
||||
<span class="underline">{{ $prot['nome_file'] }}</span>
|
||||
</button>
|
||||
<td class="px-4 py-2.5 text-center">
|
||||
@php
|
||||
$allegatiList = !empty($prot['allegati']) ? $prot['allegati'] : (!empty($prot['nome_file']) ? [['filename' => $prot['nome_file'], 'path' => $prot['file_path'], 'exists' => $prot['file_exists'], 'is_pdf' => str_ends_with(strtolower($prot['nome_file']), '.pdf'), 'is_image' => preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $prot['nome_file'])]] : []);
|
||||
@endphp
|
||||
@if(!empty($allegatiList))
|
||||
<div class="flex flex-wrap items-center justify-center gap-1.5 max-w-xs mx-auto">
|
||||
@foreach($allegatiList as $att)
|
||||
@php
|
||||
$attName = $att['filename'] ?? 'file';
|
||||
$attPath = $att['path'] ?? '';
|
||||
$attExists = $att['exists'] ?? false;
|
||||
$isPdf = $att['is_pdf'] ?? str_ends_with(strtolower($attName), '.pdf');
|
||||
$isImg = $att['is_image'] ?? preg_match('/\.(jpg|jpeg|png|webp|gif)$/i', $attName);
|
||||
@endphp
|
||||
<button
|
||||
type="button"
|
||||
wire:click="previewProtocolDoc('{{ $prot['id'] }}', '{{ addslashes($attName) }}', '{{ addslashes($attPath) }}')"
|
||||
class="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-mono transition shadow-2xs {{ $attExists ? 'bg-slate-100 hover:bg-indigo-100 hover:text-indigo-800 text-slate-700 border border-slate-200 cursor-pointer' : 'bg-slate-50 text-slate-400 border border-dashed border-slate-200 cursor-pointer' }}"
|
||||
title="{{ $attExists ? 'Visualizza anteprima (' . $attName . ')' : 'File non presente localmente (' . $attName . ')' }}"
|
||||
>
|
||||
<span>{{ $isPdf ? '📄' : ($isImg ? '🖼️' : '📎') }}</span>
|
||||
<span class="truncate max-w-[120px] {{ $attExists ? 'underline font-medium' : 'line-through' }}">{{ $attName }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<span class="text-slate-400">—</span>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -6,30 +6,22 @@ # CURRENT-205
|
|||
|
||||
## Obiettivo Completato
|
||||
|
||||
1. **Gestione Fornitore Nethome SAS (`10055221005`) & Collegamento Compensi Stabili**:
|
||||
- Fornitore ID 236 (`NETHOME sas di BARONE M. & C.`, P.IVA / CF `10055221005`) censito e collegato trasversalmente a tutti i 16 stabili in `fornitore_stabile_impostazioni`.
|
||||
- Verificate le 5 fatture elettroniche già presenti a sistema con Nethome come fornitore/cedente.
|
||||
1. **Gestione Posta Studio & 16 Stabili (IMAP, Webmail, Scheda Amministratore)**:
|
||||
- Sviluppato supporto per caselle di posta e PEC dello **Studio Amministratore** (`stabile_id = null`, storage in `storage/app/private/amministratori/{cod_adm}/studio/posta_{ordinaria|pec}/{anno}/`).
|
||||
- Webmail Posta Ordinaria e PEC dotate di filtro rapido per Stabile o Studio, pulsante per configurazione diretta IMAP e gestione stati cartelle vuote.
|
||||
- Nella Scheda Amministratore (Tab Posta / API): implementata la tabella di monitoraggio live di tutti i 16 stabili con conteggi messaggi `.eml`, stato configurazione IMAP/PEC, link diretti alla configurazione e pulsanti di test/sincronizzazione per le caselle di studio.
|
||||
|
||||
2. **Codice Mnemonico `cod_stabile` da `Stabili.mdb`**:
|
||||
- Riallineati e popolati i codici mnemonici su tutti i 16 stabili target (`0002` -> `908`, `0008` -> `909`, `0009` -> `912`, `0010` -> `13`, `0011` -> `910`, `0012` -> `920`, `0013` -> `17`, `0016` -> `145`, `0017` -> `907`, `0018` -> `18`, `0019` -> `146`, `0021` -> `148`, `0022` -> `928`, `0023` -> `8`, `0024` -> `147`, `0025` -> `223`).
|
||||
- Visibile e ricercabile nella tabella `http://192.168.0.205:8000/admin-filament/gescon/anagrafica/stabili`.
|
||||
2. **Risoluzione Multi-Allegato & Timeline Protocollo (`ArchiveFileResolver`)**:
|
||||
- Creato il resolver euristico `ArchiveFileResolver` per individuare file legacy e moderni tra radici storage e cartelle (`E_C/`, `INC_EC/`, `WA/`, `XML/`, `allegati/`, `posta/`, `documenti/`).
|
||||
- Aggiornata la scheda Unità Immobiliare (Tab Comunicazioni & Protocollo) con supporto a `posta_dett`, `corrisp_inviata`, `protoc_ec` e `communication_messages`, con rendering di chip allegati dedicati con icone, badge di presenza ed anteprima modale istantanea.
|
||||
|
||||
3. **Integrazione IMAP e Archiviazione Locale `.EML` (`php artisan gescon:fetch-mail`)**:
|
||||
- Sviluppato `ImapClient` in puro PHP SSL/TLS su porta 993 (zero dipendenze binarie libc-client) con supporto comandi IMAP standard (LOGIN, SELECT, SEARCH, FETCH BODY).
|
||||
- Sviluppato `EmlParser` per parsing MIME conforme RFC 822/2822/5322 con decodifica multipart, body HTML/testo e salvataggio allegati.
|
||||
- Salvataggio automatico del file grezzo `.eml` in `storage/app/private/amministratori/{cod_adm}/stabili/{cod_stabile}/posta_{ordinaria|pec}/{anno}/`.
|
||||
- Creazione del record in `communication_messages` e auto-matching dell'Unità Immobiliare per email del mittente.
|
||||
- Supporto configurazione e test connessione IMAP direttamente dalla scheda stabile (`/admin-filament/condomini/stabile?tab=posta-ufficiale`).
|
||||
3. **Modernizzazione Ticket Gestione & Hub Documentale**:
|
||||
- `TicketGestione`: KPI statistiche in testata, selettore filtri pill-bar, gestione rapida categorie, badge priorità/stato, filtri per fornitore operativo e gestione pratiche assicurative.
|
||||
- `DocumentiArchivio`: Hub documentale centralizzato digitale e fisico, con anteprime QR, gestione template Drive e codici mnemonici `DOC-XXXX` / `AF-X-XXXX`.
|
||||
|
||||
4. **Due Nuove Blade Webmail Stile Gmail (Posta Ordinaria e Posta PEC)**:
|
||||
- `PostaOrdinariaWebmail` (`http://192.168.0.205:8000/admin-filament/comunicazioni/posta-ordinaria`)
|
||||
- `PostaPecWebmail` (`http://192.168.0.205:8000/admin-filament/comunicazioni/posta-pec`)
|
||||
- Layout responsive modellato fedelmente sullo screenshot Gmail: barra di ricerca in tempo reale con debounce, filtro per stabile singolo o aggregato, pillole allegati (PDF, immagini, documenti), contrassegno speciale ⭐, apertura 1-click di ticket condominiale, composizione modale ed anteprima allegati rapida.
|
||||
|
||||
5. **Scheda Unità (`unita-immobiliare`) - Tab Comunicazioni & Protocollo**:
|
||||
- Aggregazione completa di corrispondenza storica (`protoc_ec`, `corrisp_inviata`) e messaggi email/PEC associati all'unità (`communication_messages`).
|
||||
- Ordinamento cronologico rigorosamente decrescente basato su Unix timestamp per evitare anomalie di ordinamento lessicografico.
|
||||
- Modal di anteprima visiva istantanea di PDF, immagini e documenti allegati cliccando sul nome del file.
|
||||
4. **Fornitore Nethome SAS (`10055221005`) & Codici Stabili**:
|
||||
- Censito e associato trasversalmente per compensi e fatturazione su tutti i 16 stabili.
|
||||
- Codici mnemonici `cod_stabile` sincronizzati da `Stabili.mdb`.
|
||||
|
||||
## Output del Giro Operativo
|
||||
|
||||
|
|
@ -37,8 +29,9 @@ ## Output del Giro Operativo
|
|||
TASK_ID: task-nethome-imap-webmail-protocollo
|
||||
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
||||
BRANCH: stabilization/205-zero
|
||||
COMMIT: d7f8346
|
||||
COMMIT: 3265562
|
||||
FILE_O_AREE_TOCCATE:
|
||||
- app/Services/Documenti/ArchiveFileResolver.php
|
||||
- app/Services/Posta/EmlParser.php
|
||||
- app/Services/Posta/ImapClient.php
|
||||
- app/Services/Posta/ImapMailboxService.php
|
||||
|
|
@ -46,23 +39,24 @@ ## Output del Giro Operativo
|
|||
- app/Filament/Pages/Posta/PostaOrdinariaWebmail.php
|
||||
- app/Filament/Pages/Posta/PostaPecWebmail.php
|
||||
- resources/views/filament/pages/posta/webmail.blade.php
|
||||
- app/Filament/Pages/Impostazioni/SchedaAmministratore.php
|
||||
- resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php
|
||||
- app/Filament/Pages/Condomini/StabilePage.php
|
||||
- resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php
|
||||
- app/Filament/Pages/UnitaImmobiliarePage.php
|
||||
- resources/views/filament/pages/unita-immobiliare.blade.php
|
||||
- app/Models/PersonaUnitaRelazione.php
|
||||
- skill-netgescon/directives/gestione-posta-imap-webmail-protocollo.md
|
||||
- resources/views/filament/pages/supporto/ticket-gestione.blade.php
|
||||
- tests/Feature/PostaImapWebmailAndProtocolloTest.php
|
||||
- skill-netgescon/control-tower/CURRENT-205.md
|
||||
TEST_ESEGUITI:
|
||||
- ./vendor/bin/pest tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (34 passed, 210 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (35 passed, 223 assertions)
|
||||
GATE_STATISTICS:
|
||||
- NETHOME_SAS_FORNITORE: Fornitore 236 configurato e collegato a tutti i 16 stabili.
|
||||
- COD_STABILE_MNEMONICO: 16 stabili aggiornati con cod_stabile da Stabili.mdb.
|
||||
- IMAP_EML_SYNC: Client socket puro PHP SSL/TLS + Parser RFC 822 + archiviazione .eml in storage per anno/stabile.
|
||||
- GMAIL_WEBMAIL_UI: Due blade dedicate (Posta Ordinaria e PEC) conformi allo screenshot di riferimento.
|
||||
- UNITA_PROTOCOLLO_MODAL: Ordinamento cronologico Unix decrescente + modal anteprima allegati/documenti.
|
||||
- TEST_SUITE: 34 test Feature passati con successo (210 asserzioni, 100% pass).
|
||||
- STUDIO_MAILBOX_INGESTION: Gestione caselle posta/PEC studio e storage locale dedicato.
|
||||
- STABILI_MAILBOX_MONITORING: Monitoraggio tabellare dei 16 stabili nella Scheda Amministratore con link diretti.
|
||||
- MULTI_ATTACHMENT_RESOLVER: `ArchiveFileResolver` operativo su 7 cartelle e storage.
|
||||
- WEBMAIL_UI_QUICK_ACTIONS: Bottone Configura IMAP, badge e filtri responsive.
|
||||
- TICKET_GESTIONE_MODERNIZATION: KPI cards, filtri di stato, gestione categorie e assegnazioni.
|
||||
- TEST_SUITE: 35 test Feature passati con successo (223 asserzioni, 100% pass).
|
||||
BLOCCO_DATI: no
|
||||
BLOCCO_CONTRATTO: no
|
||||
RISCHI_APERTI: nessuno
|
||||
|
|
@ -70,5 +64,5 @@ ## Output del Giro Operativo
|
|||
## Prossimo Passo per .200 (Validazione)
|
||||
|
||||
- Eseguire il checkout del branch `stabilization/205-zero`.
|
||||
- Eseguire la suite di test Pest (34 passed, 210 assertions).
|
||||
- Verificare la Webmail Posta Ordinaria su `http://192.168.0.205:8000/admin-filament/comunicazioni/posta-ordinaria`, la Webmail PEC su `http://192.168.0.205:8000/admin-filament/comunicazioni/posta-pec` e la Scheda Unità (tab Comunicazioni & Protocollo) su `http://192.168.0.205:8000/admin-filament/unita-immobiliare?tab=preferenze`.
|
||||
- Eseguire la suite di test Pest (35 passed, 223 assertions).
|
||||
- Verificare la Webmail Posta Ordinaria (`/admin-filament/comunicazioni/posta-ordinaria`), PEC (`/admin-filament/comunicazioni/posta-pec`), Scheda Amministratore (`/admin-filament/impostazioni/amministratore?tab=posta-api`), Ticket Gestione (`/admin-filament/supporto/ticket-gestione`) e Scheda Unità (tab Comunicazioni & Protocollo).
|
||||
|
|
|
|||
|
|
@ -165,3 +165,44 @@
|
|||
expect($stabile0016->fresh()->cod_stabile)->toBe('145');
|
||||
}
|
||||
});
|
||||
|
||||
it('ingests studio raw eml without stabile_id and resolves via ArchiveFileResolver', function () {
|
||||
$user = User::factory()->create();
|
||||
Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
$user->assignRole('amministratore');
|
||||
|
||||
$adm = \App\Models\Amministratore::firstOrCreate(
|
||||
['codice_amministratore' => 'ADMX3PD9'],
|
||||
['user_id' => $user->id, 'nome' => 'Cecilia', 'cognome' => 'Tordini']
|
||||
);
|
||||
|
||||
$service = app(ImapMailboxService::class);
|
||||
$rawEml = "From: Studio Legale <avvocato@example.com>\r\n"
|
||||
. "To: studio@amministrazione.it\r\n"
|
||||
. "Subject: Notifica studio generale\r\n"
|
||||
. "Date: Fri, 04 Sep 2026 16:00:00 +0200\r\n"
|
||||
. "Message-ID: <studio-msg-777@example.com>\r\n"
|
||||
. "MIME-Version: 1.0\r\n"
|
||||
. "Content-Type: text/plain; charset=UTF-8\r\n"
|
||||
. "\r\n"
|
||||
. "Comunicazione indirizzata direttamente allo studio amministrativo.";
|
||||
|
||||
$res = $service->ingestStudioEmlString($rawEml, $adm, [
|
||||
'tipo' => 'imap',
|
||||
'folder' => 'INBOX',
|
||||
'crea_ticket_automatico' => false,
|
||||
], false);
|
||||
|
||||
expect($res['status'])->toBe('imported');
|
||||
|
||||
$msg = CommunicationMessage::where('external_message_id', 'studio-msg-777@example.com')->first();
|
||||
expect($msg)->not->toBeNull();
|
||||
expect($msg->stabile_id)->toBeNull();
|
||||
expect($msg->metadata['is_studio'] ?? false)->toBeTrue();
|
||||
|
||||
// Test ArchiveFileResolver
|
||||
$resolved = \App\Services\Documenti\ArchiveFileResolver::resolve('0016', 'sample_non_existing.pdf');
|
||||
expect($resolved)->toHaveKeys(['filename', 'exists', 'path', 'size', 'mime', 'is_pdf', 'is_image']);
|
||||
expect($resolved['filename'])->toBe('sample_non_existing.pdf');
|
||||
expect($resolved['exists'])->toBeFalse();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user