*/ private const SMDR_PUBLIC_LINE_FALLBACKS = [ '1' => '0639731100', '01' => '0639731100', '0001' => '0639731100', '3' => '0688812703', '03' => '0688812703', '0003' => '0688812703', ]; protected static ?string $title = 'Post-it Gestione'; protected static ?string $navigationLabel = 'Post-it Gestione'; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list'; protected static ?string $slug = 'strumenti/post-it-gestione'; protected static bool $shouldRegisterNavigation = true; protected static UnitEnum|string|null $navigationGroup = 'Strumenti'; protected static ?int $navigationSort = 11; protected string $view = 'filament.pages.strumenti.post-it-gestione'; public ?int $focusPostItId = null; public string $activeTab = 'storico'; public string $tecnicoCallView = 'esterne'; public string $tecnicoDirectionFilter = 'tutte'; public string $tecnicoSearch = ''; public string $tecnicoLineFilter = ''; public string $tecnicoScopeFilter = 'tutti'; public ?string $selectedGroupModalKey = null; public string $selectedGroupModalMode = 'open'; public string $appuntiOwnerFilter = 'mine'; public ?int $selectedAppuntoId = null; /** @var array */ public array $riaperturaNote = []; /** @var array */ public array $conversioneNote = []; /** @var array */ private array $rubricaPhoneCache = []; /** @var array */ private array $pbxExtensionCache = []; /** @var array */ private array $postItMessageCache = []; /** @var array */ private array $monitoredResponseGroups = ['601', '603']; public function mount(): void { $focus = (int) request()->query('focus_post_it', 0); $this->focusPostItId = $focus > 0 ? $focus : null; if (in_array((string) request()->query('tab'), ['storico', 'appunti', 'lavagna', 'archivio'], true)) { $this->activeTab = (string) request()->query('tab'); } } public function selectAppunto(int $postItId): void { $this->selectedAppuntoId = $postItId > 0 ? $postItId : null; } public function getPostItInserimentoUrl(): string { return PostIt::getUrl(panel: 'admin-filament'); } public function getPostItPageUrl(int $postItId): string { return PostIt::getUrl(panel: 'admin-filament') . '?focus_post_it=' . $postItId; } public function convertiInTicket(int $postItId): void { if (! $this->isPostItTableReady()) { Notification::make() ->title('Tabella chiamate non pronta') ->body('Esegui prima le migrazioni per usare la gestione Post-it.') ->danger() ->send(); return; } $postIt = ChiamataPostIt::query()->find($postItId); if (! $postIt) { return; } if ($postIt->ticket_id) { Notification::make()->title('Gia convertito in ticket')->warning()->send(); return; } if (! $postIt->stabile_id) { Notification::make() ->title('Stabile mancante') ->body('Associa prima uno stabile al Post-it per poter creare il ticket.') ->danger() ->send(); return; } $ticket = Ticket::query()->create([ 'stabile_id' => $postIt->stabile_id, 'aperto_da_user_id' => Auth::id(), 'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')), 'descrizione' => $this->buildTicketDescriptionFromPostIt($postItId, $postIt), 'data_apertura' => now(), 'stato' => 'Aperto', 'priorita' => $postIt->priorita, ]); $postIt->ticket_id = $ticket->id; $postIt->stato = 'ticket'; $postIt->save(); Notification::make() ->title('Ticket creato') ->body("Ticket #{$ticket->id} creato dal Post-it #{$postIt->id}") ->success() ->send(); unset($this->conversioneNote[$postItId]); } public function chiudiPostIt(int $postItId): void { if (! $this->isPostItTableReady()) { return; } $postIt = ChiamataPostIt::query()->find($postItId); if (! $postIt) { return; } $postIt->stato = 'chiusa'; $postIt->save(); Notification::make() ->title('Post-it chiuso') ->success() ->send(); } public function riapriPostIt(int $postItId): void { if (! $this->isPostItTableReady()) { return; } $postIt = ChiamataPostIt::query()->find($postItId); if (! $postIt) { return; } $noteInput = trim((string) ($this->riaperturaNote[$postItId] ?? '')); $stamp = now()->format('d/m/Y H:i'); $noteLine = $noteInput !== '' ? '[' . $stamp . '] Riapertura: ' . $noteInput : '[' . $stamp . '] Riapertura da gestione Post-it'; $existing = trim((string) ($postIt->nota ?? '')); $postIt->nota = $existing !== '' ? ($existing . "\n" . $noteLine) : $noteLine; $postIt->stato = $postIt->ticket_id ? 'ticket' : 'post_it'; if (Schema::hasColumn('chiamate_post_it', 'chiusa_il')) { $postIt->chiusa_il = null; } $postIt->save(); if ($postIt->ticket_id) { $ticket = Ticket::query()->find((int) $postIt->ticket_id); if ($ticket && in_array((string) $ticket->stato, ['Chiuso', 'Risolto'], true)) { $ticket->stato = 'Preso in Carico'; if (Schema::hasColumn('tickets', 'data_chiusura_effettiva')) { $ticket->data_chiusura_effettiva = null; } if (Schema::hasColumn('tickets', 'data_risoluzione_effettiva')) { $ticket->data_risoluzione_effettiva = null; } $ticket->save(); if (method_exists($ticket, 'messages') && Schema::hasTable('ticket_messages')) { $ticket->messages()->create([ 'user_id' => Auth::id(), 'messaggio' => $noteLine, ]); } } } unset($this->riaperturaNote[$postItId]); Notification::make() ->title('Post-it riaperto') ->body('Riapertura completata con nota di tracciamento.') ->success() ->send(); } public function getRecentiProperty() { if (! $this->isPostItTableReady()) { return collect(); } try { $query = ChiamataPostIt::query() ->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->orderByDesc('chiamata_il') ->orderByDesc('id'); if ($this->focusPostItId) { $query->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$this->focusPostItId]); } return $query->limit(80)->get(); } catch (QueryException) { return collect(); } } public function getRecentiVisibiliProperty() { return $this->recenti ->filter(fn(ChiamataPostIt $postIt): bool => $this->shouldShowPostIt($postIt)) ->values(); } private function shouldShowPostIt(ChiamataPostIt $postIt): bool { if ($this->isInternalPostIt($postIt)) { return false; } return ! $this->isLegacyFilteredSmdrPostIt($postIt); } private function hasOperationalNoteContent(ChiamataPostIt $postIt): bool { return trim($this->extractOperationalNoteContent($postIt)) !== ''; } private function extractOperationalNoteContent(ChiamataPostIt $postIt): string { $note = trim((string) ($postIt->nota ?? '')); if ($note === '') { return ''; } $origin = strtolower(trim((string) ($postIt->origine ?? ''))); if (! in_array($origin, ['smdr', 'smdr_table', 'smdr_live_banner', 'topbar_live_call', 'panasonic_csta_table', 'csta_table'], true)) { return $note; } $lines = preg_split('/\r\n|\r|\n/', $note) ?: []; $kept = []; foreach ($lines as $line) { $line = trim((string) $line); if ($line === '') { continue; } if (preg_match('/^\d{2}\/\d{2}\/\d{2}\s+\d{2}:\d{2}\s+\d+\b/i', $line) === 1) { continue; } if (preg_match('/^chiamata ricevuta dal numero\b/i', $line) === 1) { continue; } if (preg_match('/^interno\/gruppo chiamato:\b/i', $line) === 1) { continue; } if (preg_match('/^nota operativa:\s*\d{2}\/\d{2}\/\d{2}/i', $line) === 1) { continue; } if (preg_match('/^dettaglio sorgente:\s*\d{2}\/\d{2}\/\d{2}/i', $line) === 1) { continue; } if (preg_match('/^contatto:\s*contatto non in rubrica$/i', $line) === 1) { continue; } $kept[] = $line; } return trim(implode("\n", $kept)); } public function getRecentiApertiVisibiliProperty() { return $this->recentiVisibili ->filter(fn(ChiamataPostIt $postIt): bool => (string) $postIt->stato !== 'chiusa') ->values(); } public function getRecentiChiusiVisibiliProperty() { return $this->recentiVisibili ->filter(fn(ChiamataPostIt $postIt): bool => (string) $postIt->stato === 'chiusa') ->values(); } public function getRecentiRaggruppatiProperty() { return $this->buildGroupedCollection($this->recentiApertiVisibili); } public function getRecentiChiusiRaggruppatiProperty() { return $this->buildGroupedCollection($this->recentiChiusiVisibili); } public function getAppuntiOperativiProperty() { if (! $this->isPostItTableReady()) { return collect(); } try { $query = ChiamataPostIt::query() ->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->orderByDesc('chiamata_il') ->orderByDesc('id'); $authId = (int) (Auth::id() ?? 0); if ($this->appuntiOwnerFilter === 'mine' && $authId > 0) { $query->where(function ($inner) use ($authId): void { $inner->where('creato_da_user_id', $authId) ->orWhere('assegnato_a_user_id', $authId); }); } $items = $query ->limit(200) ->get() ->filter(fn(ChiamataPostIt $postIt): bool => $this->shouldShowPostIt($postIt) && $this->hasOperationalNoteContent($postIt)) ->values(); if ($items->isNotEmpty() && ! $items->contains(fn(ChiamataPostIt $postIt): bool => (int) $postIt->id === (int) $this->selectedAppuntoId)) { $this->selectedAppuntoId = $this->focusPostItId && $items->contains(fn(ChiamataPostIt $postIt): bool => (int) $postIt->id === (int) $this->focusPostItId) ? (int) $this->focusPostItId : (int) $items->first()->id; } return $items; } catch (QueryException) { return collect(); } } public function getSelectedAppuntoProperty(): ?ChiamataPostIt { if (! $this->isPostItTableReady() || ! $this->selectedAppuntoId) { return null; } return ChiamataPostIt::query() ->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->find($this->selectedAppuntoId); } public function getOperationalNoteSummary(ChiamataPostIt $postIt, int $limit = 180): string { return Str::limit($this->extractOperationalNoteContent($postIt), $limit); } public function openGroupModal(string $modalKey, string $mode = 'open'): void { $this->selectedGroupModalKey = $modalKey; $this->selectedGroupModalMode = in_array($mode, ['open', 'closed'], true) ? $mode : 'open'; } public function closeGroupModal(): void { $this->selectedGroupModalKey = null; $this->selectedGroupModalMode = 'open'; } public function createRubricaDraftFromGroup(string $modalKey, string $mode = 'open'): void { $group = $this->findGroupedCollectionByModalKey($modalKey, $mode); if (! is_array($group)) { Notification::make()->title('Gruppo chiamate non trovato')->warning()->send(); return; } $phoneDigits = preg_replace('/\D+/', '', (string) ($group['phone'] ?? '')) ?: ''; if (! $this->canCreateRubricaFromPhone($phoneDigits)) { Notification::make() ->title('Numero non adatto alla rubrica') ->body('Per creare una nuova anagrafica serve un numero esterno valido, non un interno breve.') ->warning() ->send(); return; } $existingId = $this->resolveRubricaIdByPhone($phoneDigits); if ($existingId) { $this->redirect(RubricaUniversaleScheda::getUrl(['record' => $existingId], panel: 'admin-filament'), navigate: true); return; } $identity = $this->buildRubricaDraftIdentity((string) ($group['caller_label'] ?? ''), $phoneDigits); $user = Auth::user(); $adminId = (int) ($user?->amministratore?->id ?? 0); $rubrica = RubricaUniversale::query()->create([ 'amministratore_id' => $adminId > 0 ? $adminId : null, 'nome' => $identity['nome'], 'cognome' => $identity['cognome'], 'ragione_sociale' => $identity['ragione_sociale'], 'tipo_contatto' => $identity['tipo_contatto'], 'telefono_cellulare' => $this->isLikelyMobilePhone($phoneDigits) ? $phoneDigits : null, 'telefono_ufficio' => $this->isLikelyMobilePhone($phoneDigits) ? null : $phoneDigits, 'categoria' => 'altro', 'stato' => 'attivo', 'data_inserimento' => now()->toDateString(), 'data_ultima_modifica' => now()->toDateString(), 'creato_da' => Auth::id(), 'modificato_da' => Auth::id(), 'note_segreteria' => 'Bozza creata da Lavagna chiamate per gestire richiamo e identificazione del contatto.', ]); $itemIds = collect($group['items']) ->map(fn(ChiamataPostIt $item): int => (int) $item->id) ->filter(fn(int $id): bool => $id > 0) ->values() ->all(); if ($itemIds !== []) { ChiamataPostIt::query() ->whereIn('id', $itemIds) ->update([ 'rubrica_id' => (int) $rubrica->id, ]); } $this->rubricaPhoneCache = []; Notification::make() ->title('Bozza anagrafica creata') ->body('Nuovo contatto collegato alla lavagna chiamate. Completa ora la scheda rubrica.') ->success() ->send(); $this->redirect(RubricaUniversaleScheda::getUrl(['record' => (int) $rubrica->id], panel: 'admin-filament'), navigate: true); } public function getSelectedGroupModalProperty(): ?array { return $this->selectedGroupModalKey !== null ? $this->findGroupedCollectionByModalKey($this->selectedGroupModalKey, $this->selectedGroupModalMode) : null; } public function getSelectedGroupModalDaysProperty() { $group = $this->selectedGroupModal; if (! is_array($group)) { return collect(); } return collect($group['items']) ->groupBy(function (ChiamataPostIt $item): string { return $item->chiamata_il?->format('d/m/Y') ?: 'Data non disponibile'; }) ->map(function ($items, string $day): array { $sorted = collect($items) ->sortByDesc(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0) ->values(); return [ 'day' => $day, 'items' => $sorted, ]; }) ->values(); } public function getChiamateTecnicheProperty() { if (! Schema::hasTable('communication_messages')) { return collect(); } try { $query = CommunicationMessage::query() ->with(['assignedUser:id,name', 'stabile:id,denominazione']) ->latest('id'); if ($this->activeTab === 'smdr') { $query->where('channel', 'smdr'); } elseif ($this->activeTab === 'csta') { $query->where('channel', 'panasonic_csta'); } else { $query->whereIn('channel', ['smdr', 'panasonic_csta']); } if ($this->tecnicoCallView === 'interne') { $query->where('direction', 'internal'); } elseif ($this->tecnicoCallView === 'esterne') { $query->where('direction', '!=', 'internal'); } if (in_array($this->tecnicoDirectionFilter, ['inbound', 'outbound', 'internal'], true)) { $query->where('direction', $this->tecnicoDirectionFilter); } $search = trim($this->tecnicoSearch); if ($search !== '') { $query->where(function ($q) use ($search): void { $q->where('phone_number', 'like', '%' . $search . '%') ->orWhere('target_extension', 'like', '%' . $search . '%') ->orWhere('message_text', 'like', '%' . $search . '%') ->orWhere('sender_name', 'like', '%' . $search . '%'); }); } $items = $query ->limit(250) ->get(); if (in_array($this->tecnicoScopeFilter, ['studio', 'esterno', 'interno'], true)) { $items = $items->filter(fn(CommunicationMessage $message): bool => $this->getTecnicoScope($message) === $this->tecnicoScopeFilter)->values(); } $lineFilter = trim($this->tecnicoLineFilter); if ($lineFilter !== '') { $items = $items->filter(fn(CommunicationMessage $message): bool => $this->matchesTecnicoLineFilter($message, $lineFilter))->values(); } return $items; } catch (QueryException) { return collect(); } } public function getRecentClickToCallRequestsProperty() { if (! Schema::hasTable('pbx_click_to_call_requests')) { return collect(); } $user = Auth::user(); if (! $user instanceof User) { return collect(); } $extension = $this->normalizeExtension((string) ($user->pbx_extension ?? '')); return PbxClickToCallRequest::query() ->when($extension !== '', function ($query) use ($extension, $user): void { $query->where(function ($inner) use ($extension, $user): void { $inner->where('source_extension', $extension) ->orWhere('requested_by_user_id', (int) $user->id); }); }, fn($query) => $query->where('requested_by_user_id', (int) $user->id)) ->orderByDesc('requested_at') ->orderByDesc('id') ->limit(20) ->get(); } public function creaPostItDaMessaggio(int $messageId): void { if (! $this->isPostItTableReady()) { Notification::make()->title('Tabella post-it non pronta')->danger()->send(); return; } $message = CommunicationMessage::query()->find($messageId); if (! $message) { return; } if (! empty($message->post_it_id)) { Notification::make()->title('Post-it giĆ  associato')->warning()->send(); return; } $sourceKey = (string) ($message->channel ?: 'cti'); $sourceLabel = $sourceKey === 'panasonic_csta' ? 'Panasonic CTI' : 'SMDR'; $smdr = (array) data_get($message->metadata, 'smdr', []); $line = trim((string) ($message->message_text ?? '')); $phone = (string) ($message->phone_number ?: ($smdr['dial_number'] ?? '')); $nominativo = $this->getTecnicoNominativo($message); $lineLabel = $this->getTecnicoLineaLabel($message); $callerLabel = $this->resolveTecnicoCallerLabel($message, $phone, $nominativo); $direction = match (strtolower(trim((string) $message->direction))) { 'inbound' => 'Chiamata ricevuta', 'outbound' => 'Chiamata effettuata', 'internal' => 'Chiamata interna', default => 'Evento telefonico', }; $oggetto = $direction; if ($lineLabel !== '-') { $oggetto .= (string) $message->channel === 'smdr' ? ' - linea ' . $lineLabel : ' - ' . $lineLabel; } $notaParts = []; if ($phone !== '') { $notaParts[] = 'Numero: ' . $phone; } if ($nominativo) { $notaParts[] = 'Contatto: ' . $nominativo; } if ($line !== '') { $notaParts[] = 'Dettaglio sorgente: ' . $line; } $postIt = ChiamataPostIt::query()->create([ 'stabile_id' => $this->resolveOperationalPostItStabileId($message, $phone), 'creato_da_user_id' => Auth::id(), 'telefono' => $phone !== '' ? $phone : $message->phone_number, 'nome_chiamante' => $callerLabel !== '' ? $callerLabel : ($sourceLabel . ' ' . strtoupper((string) $message->direction)), 'direzione' => $this->normalizePostItDirection((string) $message->direction), 'origine' => $sourceKey . '_table', 'origine_id' => (string) $message->id, 'durata_secondi' => is_numeric($smdr['duration_seconds'] ?? null) ? (int) $smdr['duration_seconds'] : null, 'oggetto' => $oggetto, 'nota' => $notaParts !== [] ? implode("\n", $notaParts) : ('Evento ' . $sourceLabel . ' senza dettagli operativi'), 'priorita' => 'Media', 'stato' => 'post_it', 'chiamata_il' => $message->received_at ?? now(), ]); $message->post_it_id = $postIt->id; $message->save(); Notification::make() ->title('Post-it creato da riga tecnica') ->body('Post-it #' . $postIt->id . ' collegato al messaggio #' . $message->id) ->success() ->send(); } private function normalizePostItDirection(string $direction): string { $d = strtolower(trim($direction)); if ($d === 'inbound') { return 'in_arrivo'; } if ($d === 'outbound' || $d === 'internal') { return 'in_uscita'; } return 'in_arrivo'; } public function isPostItTableReady(): bool { return Schema::hasTable('chiamate_post_it'); } public function getRubricaUrlByPhone(?string $phone): ?string { $rubricaId = $this->resolveRubricaIdByPhone($phone); if (! $rubricaId) { return null; } return RubricaUniversaleScheda::getUrl(['record' => $rubricaId], panel: 'admin-filament'); } public function getRubricaNomeByPhone(?string $phone): ?string { $rubricaId = $this->resolveRubricaIdByPhone($phone); if (! $rubricaId) { return null; } $rubrica = RubricaUniversale::query()->find($rubricaId, ['id', 'ragione_sociale', 'nome', 'cognome']); if (! $rubrica) { return null; } $nome = trim((string) ($rubrica->ragione_sociale ?: ((string) ($rubrica->nome ?? '') . ' ' . (string) ($rubrica->cognome ?? '')))); return $nome !== '' ? $nome : null; } public function getTecnicoNominativo(CommunicationMessage $message): ?string { $extension = $this->extractExtensionFromMessage($message); if ($this->isInternalSmdrMessage($message)) { return $this->getCollaboratoreNomeByExtension($extension); } $phone = (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')); return $this->getRubricaNomeByPhone($phone); } public function getTecnicoProviderLabel(CommunicationMessage $message): string { $provider = strtolower(trim((string) data_get($message->metadata, 'provider', ''))); if ($provider === '') { $provider = (string) $message->channel; } return match ($provider) { 'smdr' => 'SMDR', 'panasonic_csta' => 'CSTA', 'panasonic_ns1000' => 'Panasonic NS1000', 'freepbx' => 'FreePBX', default => strtoupper(str_replace('_', ' ', $provider)), }; } public function getTecnicoLineaLabel(CommunicationMessage $message): string { if ((string) $message->channel === 'smdr') { return $this->resolveSmdrLineLabel($message); } $eventType = trim((string) data_get($message->metadata, 'event_type', '')); $providerCallId = trim((string) data_get($message->metadata, 'provider_call_id', '')); $parts = array_values(array_filter([$eventType, $providerCallId], fn(string $value): bool => $value !== '')); return $parts !== [] ? implode(' / ', $parts) : '-'; } public function shouldShowPostItStabile(ChiamataPostIt $postIt): bool { if (! $postIt->stabile) { return false; } if ($this->resolveGroupedRubricaId($postIt)) { return true; } $message = $this->resolveMessageFromPostIt($postIt); if ($message instanceof CommunicationMessage && ! $this->isInternalSmdrMessage($message)) { return false; } return ! $this->isGenericCallerLabel((string) ($postIt->nome_chiamante ?? ''), (string) ($postIt->telefono ?? '')); } private function matchesTecnicoLineFilter(CommunicationMessage $message, string $filter): bool { $needle = mb_strtolower(trim($filter)); if ($needle === '') { return true; } $haystacks = [ mb_strtolower($this->getTecnicoLineaLabel($message)), mb_strtolower((string) ($message->target_extension ?? '')), mb_strtolower((string) data_get($message->metadata, 'smdr.co', '')), mb_strtolower((string) data_get($message->metadata, 'provider_call_id', '')), mb_strtolower((string) ($message->message_text ?? '')), ]; foreach ($haystacks as $haystack) { if ($haystack !== '' && str_contains($haystack, $needle)) { return true; } } return false; } private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt $postIt): string { $parts = []; $nota = trim((string) ($postIt->nota ?? '')); if ($nota !== '') { $parts[] = $nota; } $conversioneNote = trim((string) ($this->conversioneNote[$postItId] ?? '')); if ($conversioneNote !== '') { $parts[] = 'Nota operatore prima della conversione:' . "\n" . $conversioneNote; } $parts[] = 'Riferimento chiamata: #' . $postIt->id; return implode("\n\n", $parts); } private function buildGroupedPostItKey(ChiamataPostIt $postIt): string { $phone = $this->normalizeGroupedPhone((string) ($postIt->telefono ?? '')); if ($phone !== '') { return 'phone:' . $phone; } $rubricaId = $this->resolveGroupedRubricaId($postIt); if ($rubricaId !== null) { return 'rubrica:' . $rubricaId; } return 'caller:' . $this->normalizeGroupedCallerKey((string) ($postIt->nome_chiamante ?? 'sconosciuto')); } private function isInternalPostIt(ChiamataPostIt $postIt): bool { $message = $this->resolveMessageFromPostIt($postIt); if ($message instanceof CommunicationMessage) { return $this->isInternalSmdrMessage($message); } $phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? '')); if (is_string($phone) && $this->isShortInternalNumber($phone)) { return true; } if (is_string($phone) && $phone !== '' && $this->isKnownPbxExtension($phone)) { return true; } return false; } private function isLegacyFilteredSmdrPostIt(ChiamataPostIt $postIt): bool { if ((string) ($postIt->origine ?? '') !== 'smdr') { return false; } $raw = strtoupper(trim((string) ($postIt->nota ?? ''))); if ($raw === '') { return false; } return ! str_contains($raw, ''); } private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage { $originKey = trim((string) ($postIt->origine_id ?? '')); if ($originKey === '') { return null; } if (array_key_exists($originKey, $this->postItMessageCache)) { return $this->postItMessageCache[$originKey]; } $message = null; if (ctype_digit($originKey)) { $message = CommunicationMessage::query()->find((int) $originKey); } elseif ((string) ($postIt->origine ?? '') === 'smdr') { $message = CommunicationMessage::query() ->where('channel', 'smdr') ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.smdr.fingerprint')) = ?", [$originKey]) ->orderByDesc('id') ->first(); } $this->postItMessageCache[$originKey] = $message; return $message; } private function normalizeGroupedCallerKey(string $value): string { $normalized = preg_replace('/\s+/', ' ', trim(mb_strtolower($value))) ?? ''; return $normalized !== '' ? $normalized : 'sconosciuto'; } private function normalizeGroupedPhone(string $phone): string { $digits = preg_replace('/\D+/', '', $phone) ?: ''; return $this->isShortInternalNumber($digits) ? '' : $digits; } private function resolveGroupedRubricaId(ChiamataPostIt $postIt): ?int { $rubricaId = (int) ($postIt->rubrica_id ?? 0); if ($rubricaId > 0) { return $rubricaId; } return $this->resolveRubricaIdByPhone((string) ($postIt->telefono ?? '')); } /** * @param array $items */ private function resolveLatestGroupedPostIt(array $items): ChiamataPostIt { usort($items, function (ChiamataPostIt $left, ChiamataPostIt $right): int { return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0); }); return $items[0]; } /** * @param array $items */ private function resolveGroupedCallerLabel(array $items): string { foreach ($items as $item) { $rubricaId = $this->resolveGroupedRubricaId($item); if ($rubricaId) { $rubrica = RubricaUniversale::query()->find($rubricaId, ['ragione_sociale', 'nome', 'cognome']); if ($rubrica) { $label = trim((string) ($rubrica->ragione_sociale ?: (($rubrica->nome ?? '') . ' ' . ($rubrica->cognome ?? '')))); if ($label !== '') { return $label; } } } $label = trim((string) ($item->nome_chiamante ?? '')); if ($label !== '' && ! $this->isGenericCallerLabel($label, (string) ($item->telefono ?? ''))) { return $label; } } return 'Contatto non in Rubrica'; } private function resolveGroupedPhoneLabel(array $items): string { foreach ($items as $item) { $phone = trim((string) ($item->telefono ?? '')); if ($phone !== '') { return $phone; } } return ''; } private function buildGroupedCollection($items) { $groups = []; foreach ($items as $postIt) { if ($this->isInternalPostIt($postIt)) { continue; } $groupKey = $this->buildGroupedPostItKey($postIt); if (isset($groups[$groupKey])) { $groups[$groupKey]['items'][] = $postIt; $groups[$groupKey]['count']++; continue; } $groups[$groupKey] = [ 'key' => $groupKey, 'modal_key' => md5($groupKey), 'items' => [$postIt], 'count' => 1, 'latest' => $postIt, 'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato', 'phone' => (string) ($postIt->telefono ?? ''), 'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-', 'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-', ]; } return collect(array_values($groups)) ->map(function (array $group): array { usort($group['items'], function (ChiamataPostIt $left, ChiamataPostIt $right): int { return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0); }); $group['latest'] = $this->resolveLatestGroupedPostIt($group['items']); $group['primary_post_it'] = $this->resolvePrimaryGroupedPostIt($group['items']); $oldest = collect($group['items']) ->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0) ->first(); $group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-'; $group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-'; $group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']); $group['phone'] = $this->resolveGroupedPhoneLabel($group['items']); $group['rubrica_url'] = $this->getRubricaUrlByPhone((string) $group['phone']); $group['answered_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))->count(); $group['missed_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isMissedPostIt($item))->count(); $group['can_create_rubrica'] = ! $group['rubrica_url'] && $this->canCreateRubricaFromPhone((string) $group['phone']); $group['primary_action_label'] = $group['answered_count'] > 0 ? 'Apri Post-it' : 'Apri richiamo'; $group['visible_items'] = collect($group['items'])->take(4)->values()->all(); $group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items'])); return $group; }) ->sortByDesc(fn(array $group) => $group['latest']->chiamata_il?->getTimestamp() ?? 0) ->values(); } private function resolveOperationalPostItStabileId(CommunicationMessage $message, string $phone): ?int { if ($this->isInternalSmdrMessage($message)) { return (int) ($message->stabile_id ?? 0) ?: null; } if ($this->resolveRubricaIdByPhone($phone)) { return (int) ($message->stabile_id ?? 0) ?: null; } return null; } private function resolveTecnicoCallerLabel(CommunicationMessage $message, string $phone, ?string $nominativo): string { $label = trim((string) $nominativo); if ($label !== '') { return $label; } if ($this->isInternalSmdrMessage($message)) { $extension = $this->extractExtensionFromMessage($message); return $extension ? 'Interno ' . $extension : 'Interno PBX'; } return 'Contatto non in Rubrica'; } private function resolveSmdrLineLabel(CommunicationMessage $message): string { $stored = trim((string) data_get($message->metadata, 'smdr.line_label', '')); if ($stored !== '') { return $stored; } $rawLine = trim((string) data_get($message->metadata, 'smdr.co', '')); if ($rawLine === '') { return '-'; } $routing = app(PbxRoutingService::class)->resolveByIncomingLine($rawLine); $lineLabel = trim((string) ($routing['line_label'] ?? '')); if ($lineLabel !== '') { return $lineLabel; } $normalized = app(PbxRoutingService::class)->normalizeLineNumber($rawLine); return self::SMDR_PUBLIC_LINE_FALLBACKS[$normalized] ?? $rawLine; } private function isGenericCallerLabel(string $label, string $phone = ''): bool { $normalizedLabel = mb_strtolower(trim($label)); if ($normalizedLabel === '') { return true; } if (in_array($normalizedLabel, ['chiamante non identificato', 'contatto non in rubrica'], true)) { return true; } if (str_starts_with($normalizedLabel, 'smdr ') || str_starts_with($normalizedLabel, 'panasonic cti ')) { return true; } $labelDigits = preg_replace('/\D+/', '', $label) ?: ''; $phoneDigits = preg_replace('/\D+/', '', $phone) ?: ''; return $labelDigits !== '' && $phoneDigits !== '' && $labelDigits === $phoneDigits; } public function richiediClickToCallDaMessaggio(int $messageId): void { $user = Auth::user(); if (! $user instanceof User) { return; } $extension = $this->normalizeExtension((string) ($user->pbx_extension ?? '')); if (! (bool) ($user->pbx_click_to_call_enabled ?? false) || $extension === '') { Notification::make()->title('Click-to-call non abilitato per il tuo utente')->warning()->send(); return; } $message = CommunicationMessage::query()->find($messageId); if (! $message) { Notification::make()->title('Messaggio tecnico non trovato')->warning()->send(); return; } $phone = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''))); if (! is_string($phone) || $phone === '') { Notification::make()->title('Numero non valido')->warning()->send(); return; } PbxClickToCallRequest::query()->create([ 'requested_by_user_id' => (int) $user->id, 'assigned_user_id' => (int) $user->id, 'stabile_id' => $message->stabile_id, 'communication_message_id' => (int) $message->id, 'source_extension' => $extension, 'target_number' => $phone, 'status' => 'pending', 'note' => 'Richiesta da Post-it Gestione [' . $this->getTecnicoProviderLabel($message) . ']', 'requested_at' => now(), 'metadata' => [ 'requested_from' => 'post_it_gestione', 'source_channel' => (string) $message->channel, 'provider' => (string) data_get($message->metadata, 'provider', $message->channel), 'target_extension' => (string) ($message->target_extension ?? ''), ], ]); Notification::make() ->title('Richiesta click-to-call registrata') ->body('Interno ' . $extension . ' -> ' . $phone) ->success() ->send(); } public function getCollaboratoreNomeByExtension(?string $extension): ?string { $normalized = $this->normalizeExtension($extension); if ($normalized === null) { return null; } if (array_key_exists($normalized, $this->pbxExtensionCache)) { return $this->pbxExtensionCache[$normalized]['name']; } if (! Schema::hasColumn('users', 'pbx_extension')) { $this->pbxExtensionCache[$normalized] = ['id' => null, 'name' => null]; return null; } $user = User::query() ->select(['id', 'name', 'pbx_extension']) ->where('pbx_extension', $normalized) ->first(); $result = [ 'id' => $user ? (int) $user->id : null, 'name' => $user ? trim((string) $user->name) : null, ]; $this->pbxExtensionCache[$normalized] = $result; return $result['name']; } public function isInternalSmdrMessage(CommunicationMessage $message): bool { if ((string) $message->direction === 'internal') { return true; } $extension = $this->extractExtensionFromMessage($message); $phone = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''))); return $extension !== null && $phone !== '' && $phone === $extension; } public function getTecnicoInternoLabel(CommunicationMessage $message): string { $extension = $this->extractExtensionFromMessage($message); if ($extension === null) { return '-'; } $name = $this->getCollaboratoreNomeByExtension($extension); if ($name === null) { return $extension; } return $extension . ' - ' . $name; } public function getTecnicoScope(CommunicationMessage $message): string { if ($this->isInternalSmdrMessage($message)) { return 'interno'; } if ($this->isManagedStudioNumberMessage($message)) { return 'studio'; } return 'esterno'; } public function getTecnicoScopeLabel(CommunicationMessage $message): string { return match ($this->getTecnicoScope($message)) { 'interno' => 'Interno collaboratore', 'studio' => 'Numero studio gestito', default => 'Numero esterno', }; } private function resolveRubricaIdByPhone(?string $phone): ?int { $digits = preg_replace('/\D+/', '', (string) $phone); if (! is_string($digits) || $digits === '') { return null; } if ($this->isShortInternalNumber($digits)) { return null; } if ($this->isKnownPbxExtension($digits)) { return null; } if (array_key_exists($digits, $this->rubricaPhoneCache)) { return $this->rubricaPhoneCache[$digits]; } $adminId = (int) (Auth::user()?->amministratore?->id ?? 0); $result = app(RubricaPhoneLookupService::class)->findByPhone($digits, $adminId)?->id; $result = is_numeric($result) ? (int) $result : null; $this->rubricaPhoneCache[$digits] = $result; return $result; } private function extractExtensionFromMessage(CommunicationMessage $message): ?string { $extension = (string) ($message->target_extension ?: data_get($message->metadata, 'smdr.extension', '')); return $this->normalizeExtension($extension); } private function normalizeExtension(?string $extension): ?string { $digits = preg_replace('/\D+/', '', (string) $extension); return is_string($digits) && $digits !== '' ? $digits : null; } private function isKnownPbxExtension(string $digits): bool { if (strlen($digits) > 6 || ! Schema::hasColumn('users', 'pbx_extension')) { return false; } return User::query()->where('pbx_extension', $digits)->exists(); } private function isShortInternalNumber(string $digits): bool { return $digits !== '' && strlen($digits) <= 4; } private function canCreateRubricaFromPhone(?string $phone): bool { $digits = preg_replace('/\D+/', '', (string) $phone) ?: ''; return $digits !== '' && ! $this->isShortInternalNumber($digits); } private function isLikelyMobilePhone(string $digits): bool { return str_starts_with($digits, '3') && strlen($digits) >= 9; } private function normalizePostItOutcome(?string $outcome): string { return Str::lower(trim((string) $outcome)); } public function isAnsweredPostIt(ChiamataPostIt $postIt): bool { if ($this->isMissedResponseGroupPostIt($postIt)) { return false; } $outcome = $this->normalizePostItOutcome($postIt->esito); if (($postIt->durata_secondi ?? 0) > 0) { return true; } return in_array($outcome, ['answered', 'risposta', 'gestita', 'connected', 'ok'], true); } private function isMissedPostIt(ChiamataPostIt $postIt): bool { if ($this->isMissedResponseGroupPostIt($postIt)) { return true; } $outcome = $this->normalizePostItOutcome($postIt->esito); if (in_array($outcome, ['persa', 'missed', 'no_answer', 'unanswered', 'non_risposta'], true)) { return true; } return (string) $postIt->direzione === 'in_arrivo' && ! $this->isAnsweredPostIt($postIt) && (int) ($postIt->durata_secondi ?? 0) === 0; } private function isMissedResponseGroupPostIt(ChiamataPostIt $postIt): bool { if ((string) $postIt->direzione !== 'in_arrivo') { return false; } $message = $this->resolveMessageFromPostIt($postIt); if (! $message instanceof CommunicationMessage || (string) $message->channel !== 'smdr') { return false; } $groupExtension = $this->normalizeExtension((string) data_get($message->metadata, 'smdr.extension', '')); if (! $this->isMonitoredResponseGroupExtension($groupExtension)) { $groupExtension = $this->normalizeExtension((string) ($message->target_extension ?? '')); } if (! $this->isMonitoredResponseGroupExtension($groupExtension)) { return false; } $answerExtensions = array_values(array_unique(array_filter([ $this->normalizeExtension((string) data_get($message->metadata, 'smdr.target_extension', '')), $this->normalizeExtension((string) ($message->target_extension ?? '')), ]))); foreach ($answerExtensions as $answerExtension) { if (! $this->isMonitoredResponseGroupExtension($answerExtension)) { return false; } } return true; } private function isMonitoredResponseGroupExtension(?string $extension): bool { if ($extension === null || $extension === '') { return false; } return in_array($extension, $this->monitoredResponseGroups, true); } public function getPostItCallStateMeta(ChiamataPostIt $postIt): array { if ($this->isMissedPostIt($postIt)) { return [ 'label' => 'Mancata risposta', 'class' => 'bg-rose-100 text-rose-800', ]; } if ($this->isAnsweredPostIt($postIt)) { return [ 'label' => (string) $postIt->direzione === 'in_uscita' ? 'Richiamo effettuato' : 'Risposta registrata', 'class' => 'bg-emerald-100 text-emerald-800', ]; } return [ 'label' => (string) $postIt->direzione === 'in_uscita' ? 'Richiamo da fare' : 'Da verificare', 'class' => 'bg-amber-100 text-amber-900', ]; } /** @param array $items */ private function resolvePrimaryGroupedPostIt(array $items): ChiamataPostIt { $answered = collect($items) ->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item)) ->sortByDesc(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0) ->values(); if ($answered->isNotEmpty()) { return $answered->first(); } return $this->resolveLatestGroupedPostIt($items); } private function findGroupedCollectionByModalKey(string $modalKey, string $mode = 'open'): ?array { $groups = $mode === 'closed' ? $this->recentiChiusiRaggruppati : $this->recentiRaggruppati; $group = $groups->firstWhere('modal_key', $modalKey); return is_array($group) ? $group : null; } private function buildRubricaDraftIdentity(string $label, string $phoneDigits): array { $cleanLabel = trim(preg_replace('/\s+/', ' ', $label) ?? ''); $cleanLabel = $cleanLabel !== '' && $cleanLabel !== 'Chiamante non identificato' ? $cleanLabel : ('Nuovo contatto ' . $phoneDigits); if (str_contains($cleanLabel, '/') || preg_match('/\b(srl|sas|spa|snc|studio|condominio|associazione|impresa)\b/i', $cleanLabel) === 1) { return [ 'tipo_contatto' => 'persona_giuridica', 'nome' => null, 'cognome' => null, 'ragione_sociale' => $cleanLabel, ]; } $parts = preg_split('/\s+/', $cleanLabel) ?: []; if (count($parts) >= 2) { $nome = array_shift($parts); return [ 'tipo_contatto' => 'persona_fisica', 'nome' => $nome, 'cognome' => implode(' ', $parts), 'ragione_sociale' => null, ]; } return [ 'tipo_contatto' => 'persona_fisica', 'nome' => $cleanLabel, 'cognome' => null, 'ragione_sociale' => null, ]; } private function isManagedStudioNumberMessage(CommunicationMessage $message): bool { $phoneDigits = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''))); if (! is_string($phoneDigits) || $phoneDigits === '') { return false; } return in_array($phoneDigits, $this->getManagedStudioNumbers(), true); } /** @return array */ private function getManagedStudioNumbers(): array { $user = Auth::user(); if (! $user) { return []; } $impostazioni = (array) (($user->amministratore?->impostazioni ?? [])); $centralino = (array) ($impostazioni['centralino'] ?? []); $numbers = []; foreach (['numero_principale', 'numero_backup', 'numero_emergenza'] as $key) { $digits = preg_replace('/\D+/', '', (string) ($centralino[$key] ?? '')); if (is_string($digits) && $digits !== '') { $numbers[] = $digits; } } return array_values(array_unique($numbers)); } public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } }