diff --git a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php index e11e222..31b82b2 100644 --- a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php +++ b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php @@ -2,6 +2,7 @@ namespace App\Filament\Pages\Impostazioni; use App\Models\Amministratore; +use App\Models\CommunicationMessage; use App\Models\Fornitore; use App\Models\FornitoreDipendente; use App\Models\Stabile; @@ -718,6 +719,128 @@ public function form(Schema $schema): Schema ]), ]), + Tab::make('PBX / TAPI') + ->schema([ + Section::make('Profilo centralino e bridge') + ->columns(3) + ->schema([ + Select::make('impostazioni.pbx.provider') + ->label('Tipo centralino') + ->options([ + 'panasonic_ns1000' => 'Panasonic NS1000', + 'panasonic_generic' => 'Panasonic generico', + 'wildix' => 'Wildix', + '3cx' => '3CX', + 'custom' => 'Altro / custom', + ]) + ->default('panasonic_ns1000'), + Select::make('impostazioni.pbx.channel') + ->label('Canale CRM') + ->options([ + 'panasonic_csta' => 'Panasonic CSTA / CTI', + 'tapi' => 'Windows TAPI', + 'hybrid' => 'Ibrido TAPI + CSTA', + ]) + ->default('hybrid'), + Select::make('impostazioni.pbx.bridge_mode') + ->label('Modalita bridge') + ->options([ + 'group-first' => 'Group-first', + 'extension-first' => 'Extension-first', + 'none' => 'Solo monitoraggio', + ]) + ->default('group-first'), + TextInput::make('impostazioni.pbx.tapi_host') + ->label('Host Windows watcher') + ->maxLength(255) + ->placeholder('Es. ws-panasonic-01'), + TextInput::make('impostazioni.pbx.tapi_provider') + ->label('Provider TAPI / TSP') + ->maxLength(255) + ->default('CTSP0000.tsp'), + TextInput::make('impostazioni.pbx.watch_extensions') + ->label('Interni monitorati') + ->maxLength(255) + ->placeholder('201,205,206,601,603') + ->helperText('Lista CSV degli interni/gruppi che il watcher Windows deve osservare.'), + Toggle::make('impostazioni.pbx.popup_enabled') + ->label('Popup CRM attivi') + ->default(true), + Toggle::make('impostazioni.pbx.click_to_call_enabled') + ->label('Click-to-call attivo') + ->default(true), + Textarea::make('impostazioni.pbx.routing_notes') + ->label('Note routing PBX') + ->rows(3) + ->columnSpanFull(), + ]), + + Section::make('Gruppi di risposta') + ->schema([ + Repeater::make('impostazioni.pbx.response_groups') + ->label('Gruppi di risposta Panasonic') + ->defaultItems(0) + ->schema([ + TextInput::make('extension') + ->label('Interno gruppo') + ->maxLength(20) + ->placeholder('601'), + TextInput::make('label') + ->label('Descrizione') + ->maxLength(120) + ->placeholder('Gruppo giorno'), + TextInput::make('mode') + ->label('Modalita') + ->maxLength(60) + ->placeholder('giorno / notte / backup'), + TextInput::make('target_extension') + ->label('Interno di fallback') + ->maxLength(20) + ->placeholder('205'), + ]) + ->columns(4) + ->columnSpanFull(), + ]), + + Section::make('Linee entranti e DID') + ->schema([ + Repeater::make('impostazioni.pbx.incoming_lines') + ->label('Linee in entrata') + ->defaultItems(0) + ->schema([ + TextInput::make('number') + ->label('Numero / DID') + ->maxLength(40) + ->placeholder('0811234567'), + TextInput::make('label') + ->label('Descrizione linea') + ->maxLength(120) + ->placeholder('Linea assistenza'), + TextInput::make('route_group') + ->label('Gruppo target') + ->maxLength(20) + ->placeholder('601'), + TextInput::make('target_extension') + ->label('Interno target') + ->maxLength(20) + ->placeholder('205'), + Textarea::make('notes') + ->label('Note') + ->rows(2) + ->columnSpanFull(), + ]) + ->columns(4) + ->columnSpanFull(), + ]), + + Section::make('Cruscotto chiamate Panasonic / TAPI') + ->schema([ + Placeholder::make('pbx_tapi_panel') + ->hiddenLabel() + ->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-pbx-tapi')), + ]), + ]), + Tab::make('Operativita / Accessi') ->schema([ Section::make('Azioni operative e gestione accessi') @@ -729,7 +852,7 @@ public function form(Schema $schema): Schema ]), ]), ]); - } + } public function runProductionUpgrade(): void { @@ -1189,6 +1312,219 @@ public function getCollaboratoriCentralinoRowsProperty(): array ->all(); } + /** + * @return array{status:string,label:string,provider:string,channel:string,bridge_mode:string,watched_extensions:array,response_groups:array>,incoming_lines:array>,last_event_at:?string,last_event_type:?string,total_messages:int,last_24h:int,missed_calls:int,assessment:string} + */ + public function getPbxSummaryProperty(): array + { + $settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []); + $provider = (string) ($settings['provider'] ?? 'panasonic_ns1000'); + $channel = (string) ($settings['channel'] ?? 'hybrid'); + $bridgeMode = (string) ($settings['bridge_mode'] ?? 'group-first'); + $watchedExtensions = $this->resolveWatchedPbxExtensions($settings); + $responseGroups = array_values(array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row))); + $incomingLines = array_values(array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row))); + + if (! Schema::hasTable('communication_messages')) { + return [ + 'status' => ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) ? 'configured' : 'to-configure', + 'label' => ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) ? 'Configurato, in attesa eventi' : 'Da configurare', + 'provider' => $provider, + 'channel' => $channel, + 'bridge_mode' => $bridgeMode, + 'watched_extensions' => $watchedExtensions, + 'response_groups' => $responseGroups, + 'incoming_lines' => $incomingLines, + 'last_event_at' => null, + 'last_event_type' => null, + 'total_messages' => 0, + 'last_24h' => 0, + 'missed_calls' => 0, + 'assessment' => 'La tabella communication_messages non e disponibile in questo ambiente, quindi il cruscotto mostra solo la configurazione PBX.', + ]; + } + + $query = $this->pbxMessagesQuery(); + $latest = (clone $query)->orderByDesc('received_at')->orderByDesc('id')->first(); + $total = (clone $query)->count(); + $last24h = (clone $query)->where('received_at', '>=', now()->subDay())->count(); + $missedCalls = (clone $query)->where(function ($q): void { + $q->where('message_text', 'like', '%persa%') + ->orWhere('metadata->outcome', 'like', '%miss%') + ->orWhere('metadata->outcome', 'like', '%no_answer%'); + })->count(); + + $status = 'to-configure'; + $label = 'Da configurare'; + if ($total > 0) { + $status = 'active'; + $label = 'CRM collegato'; + } elseif ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) { + $status = 'configured'; + $label = 'Configurato, in attesa eventi'; + } + + $assessment = $bridgeMode === 'group-first' + ? 'Con modalita group-first il watcher Windows puo vedere eventi sugli interni singoli ma il bridge usa come sorgente autorevole i gruppi di risposta. Questo e coerente con i log Panasonic gia acquisiti.' + : 'La configurazione e predisposta per usare gli eventi PBX direttamente nel CRM. Verifica i log del watcher Windows per confermare il flusso sul canale selezionato.'; + + return [ + 'status' => $status, + 'label' => $label, + 'provider' => $provider, + 'channel' => $channel, + 'bridge_mode' => $bridgeMode, + 'watched_extensions'=> $watchedExtensions, + 'response_groups' => $responseGroups, + 'incoming_lines' => $incomingLines, + 'last_event_at' => $latest?->received_at?->format('d/m/Y H:i:s'), + 'last_event_type' => $latest ? (string) data_get($latest->metadata, 'event_type', $latest->status) : null, + 'total_messages' => $total, + 'last_24h' => $last24h, + 'missed_calls' => $missedCalls, + 'assessment' => $assessment, + ]; + } + + /** + * @return array> + */ + public function getPbxRecentMessagesProperty(): array + { + if (! Schema::hasTable('communication_messages')) { + return []; + } + + return $this->pbxMessagesQuery() + ->with('assignedUser:id,name') + ->orderByDesc('received_at') + ->orderByDesc('id') + ->limit(12) + ->get(['id', 'phone_number', 'target_extension', 'assigned_user_id', 'direction', 'status', 'received_at', 'metadata']) + ->map(fn(CommunicationMessage $message): array => [ + 'id' => (int) $message->id, + 'received_at' => $message->received_at?->format('d/m/Y H:i:s') ?? '-', + 'phone_number' => (string) ($message->phone_number ?? '-'), + 'target_extension' => (string) ($message->target_extension ?? data_get($message->metadata, 'called_extension', '-')), + 'direction' => (string) ($message->direction ?? '-'), + 'status' => (string) ($message->status ?? '-'), + 'event_type' => (string) data_get($message->metadata, 'event_type', '-'), + 'provider' => (string) data_get($message->metadata, 'provider', $message->channel), + 'user' => (string) ($message->assignedUser?->name ?? '-'), + 'studio_role' => (string) data_get($message->metadata, 'studio_role', '-'), + 'studio_mode' => (string) data_get($message->metadata, 'studio_mode', '-'), + ]) + ->all(); + } + + /** + * @return array> + */ + public function getPbxRoutingRowsProperty(): array + { + $settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []); + $centralino = (array) Arr::get($this->amministratore->impostazioni ?? [], 'centralino', []); + $groupRows = array_values(array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row))); + $incoming = array_values(array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row))); + $routingRows = [ + [ + 'type' => 'Studio', + 'label' => 'Interno centralino', + 'extension' => (string) Arr::get($centralino, 'interno_centralino', '-'), + 'target' => 'Smistamento studio', + 'note' => (string) Arr::get($centralino, 'numero_principale', '-'), + ], + [ + 'type' => 'Studio', + 'label' => 'Operatore', + 'extension' => (string) Arr::get($centralino, 'interno_operatore', '-'), + 'target' => 'Utente operatore', + 'note' => 'Instradamento diretto', + ], + [ + 'type' => 'Studio', + 'label' => 'Amministratore', + 'extension' => (string) Arr::get($centralino, 'interno_amministratore', '-'), + 'target' => 'Utente amministratore', + 'note' => 'Instradamento diretto', + ], + [ + 'type' => 'Gruppo', + 'label' => 'Gruppo giorno', + 'extension' => (string) Arr::get($centralino, 'interno_gruppo_giorno', '-'), + 'target' => 'Modalita giorno', + 'note' => 'Routing shared', + ], + [ + 'type' => 'Gruppo', + 'label' => 'Gruppo notte', + 'extension' => (string) Arr::get($centralino, 'interno_gruppo_notte', '-'), + 'target' => 'Modalita notte', + 'note' => 'Routing shared', + ], + ]; + + foreach ($groupRows as $row) { + $routingRows[] = [ + 'type' => 'Gruppo', + 'label' => (string) ($row['label'] ?? 'Gruppo risposta'), + 'extension' => (string) ($row['extension'] ?? '-'), + 'target' => (string) ($row['target_extension'] ?? '-'), + 'note' => (string) ($row['mode'] ?? '-'), + ]; + } + + foreach ($incoming as $row) { + $routingRows[] = [ + 'type' => 'Linea', + 'label' => (string) ($row['label'] ?? 'Linea entrante'), + 'extension' => (string) ($row['number'] ?? '-'), + 'target' => trim((string) (($row['route_group'] ?? '') !== '' ? $row['route_group'] : ($row['target_extension'] ?? '-'))), + 'note' => (string) ($row['notes'] ?? '-'), + ]; + } + + return $routingRows; + } + + private function pbxMessagesQuery() + { + $extensions = $this->resolveWatchedPbxExtensions((array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', [])); + + return CommunicationMessage::query() + ->where('channel', 'panasonic_csta') + ->where(function ($q) use ($extensions): void { + $q->where('metadata->amministratore_id', (int) $this->amministratore->id); + + if ($extensions !== [] && Schema::hasColumn('communication_messages', 'target_extension')) { + $q->orWhereIn('target_extension', $extensions); + } + }); + } + + /** + * @return array + */ + private function resolveWatchedPbxExtensions(array $settings): array + { + $centralinoExtensions = [ + (string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_centralino', ''), + (string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_operatore', ''), + (string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_amministratore', ''), + (string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_gruppo_giorno', ''), + (string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_gruppo_notte', ''), + ]; + + $csvExtensions = preg_split('/\s*,\s*/', (string) ($settings['watch_extensions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: []; + $groupExtensions = array_map(fn($row): string => (string) ($row['extension'] ?? ''), array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row))); + $lineTargets = array_map(fn($row): string => (string) (($row['target_extension'] ?? '') !== '' ? $row['target_extension'] : ($row['route_group'] ?? '')), array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row))); + + return array_values(array_unique(array_filter(array_map( + fn($value): string => preg_replace('/\D+/', '', (string) $value) ?: '', + array_merge($centralinoExtensions, $csvExtensions, $groupExtensions, $lineTargets) + )))); + } + public function salvaInternoCollaboratore(int $userId): void { $user = User::query()->find($userId); diff --git a/app/Filament/Pages/Strumenti/PostItGestione.php b/app/Filament/Pages/Strumenti/PostItGestione.php index b713076..5b88b69 100644 --- a/app/Filament/Pages/Strumenti/PostItGestione.php +++ b/app/Filament/Pages/Strumenti/PostItGestione.php @@ -4,6 +4,7 @@ use App\Filament\Pages\Gescon\RubricaUniversaleScheda; use App\Models\ChiamataPostIt; use App\Models\CommunicationMessage; +use App\Models\PbxClickToCallRequest; use App\Models\RubricaUniversale; use App\Models\Ticket; use App\Models\User; @@ -214,9 +215,16 @@ public function getChiamateTecnicheProperty() try { $query = CommunicationMessage::query() ->with(['assignedUser:id,name', 'stabile:id,denominazione']) - ->whereIn('channel', ['smdr', 'panasonic_csta']) ->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') { @@ -251,6 +259,32 @@ public function getChiamateTecnicheProperty() } } + 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()) { @@ -268,10 +302,10 @@ public function creaPostItDaMessaggio(int $messageId): void return; } - $sourceKey = (string) ($message->channel ?: 'cti'); + $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 ?? '')); + $smdr = (array) data_get($message->metadata, 'smdr', []); + $line = trim((string) ($message->message_text ?? '')); $postIt = ChiamataPostIt::query()->create([ 'stabile_id' => $message->stabile_id, @@ -355,6 +389,86 @@ public function getTecnicoNominativo(CommunicationMessage $message): ?string 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') { + $line = trim((string) data_get($message->metadata, 'smdr.co', '')); + return $line !== '' ? $line : '-'; + } + + $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 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); diff --git a/app/Filament/Pages/Supporto/TicketGestione.php b/app/Filament/Pages/Supporto/TicketGestione.php index 6b2c688..1be7387 100644 --- a/app/Filament/Pages/Supporto/TicketGestione.php +++ b/app/Filament/Pages/Supporto/TicketGestione.php @@ -13,6 +13,7 @@ use App\Models\User; use App\Services\Documenti\ImageTextExtractionService; use App\Services\Documenti\PdfTextExtractionService; +use App\Services\Support\TicketAttachmentUploadService; use App\Support\StabileContext; use BackedEnum; use Filament\Notifications\Notification; @@ -540,21 +541,16 @@ public function caricaAllegati(): void continue; } - $path = $file->store('ticket-gestione/' . $ticket->id, 'public'); - $mime = TicketAttachment::normalizeMimeType( - method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '', - method_exists($file, 'getClientOriginalName') ? (string) $file->getClientOriginalName() : '', - $path - ); + $stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-gestione/' . $ticket->id); $attachment = TicketAttachment::query()->create([ 'ticket_id' => $ticket->id, 'ticket_update_id' => null, 'user_id' => Auth::id(), - 'file_path' => $path, - 'original_file_name' => (string) $file->getClientOriginalName(), - 'mime_type' => $mime, - 'size' => (int) $file->getSize(), + 'file_path' => $stored['path'], + 'original_file_name' => $stored['original_name'], + 'mime_type' => $stored['mime'], + 'size' => $stored['size'], 'description' => $this->descrizioneAllegati ?: 'Allegato da gestione ticket', ]); diff --git a/app/Filament/Pages/Supporto/TicketMobile.php b/app/Filament/Pages/Supporto/TicketMobile.php index b092877..af22c04 100644 --- a/app/Filament/Pages/Supporto/TicketMobile.php +++ b/app/Filament/Pages/Supporto/TicketMobile.php @@ -5,15 +5,14 @@ use App\Filament\Pages\Gescon\RubricaUniversaleScheda; use App\Models\CategoriaTicket; use App\Models\ChiamataPostIt; -use App\Models\CommunicationMessage; use App\Models\PbxClickToCallRequest; use App\Models\RubricaUniversale; -use App\Models\Soggetto; use App\Models\Stabile; use App\Models\Ticket; use App\Models\TicketAttachment; use App\Models\User; -use App\Services\Cti\PbxRoutingService; +use App\Services\Cti\LiveIncomingCallService; +use App\Services\Support\TicketAttachmentUploadService; use App\Support\StabileContext; use BackedEnum; use Filament\Notifications\Notification; @@ -130,6 +129,7 @@ public function mount(): void } $this->refreshLiveCallBanner(); + $this->applyLiveCallQueryPrefill(); $this->refreshData(); } @@ -160,54 +160,8 @@ public function refreshLiveCallBanner(): void return; } - $userExtension = trim((string) ($authUser->pbx_extension ?? '')); - $watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($authUser); - $popupRestricted = (bool) ($authUser->pbx_popup_enabled ?? false) && count($watchedExtensions) > 0; - - $latest = CommunicationMessage::query() - ->whereIn('channel', ['smdr', 'panasonic_csta']) - ->where('direction', 'inbound') - ->whereNotNull('received_at') - ->where('received_at', '>=', now()->subMinutes(8)) - ->when($popupRestricted, function ($q) use ($watchedExtensions): void { - $q->whereIn('target_extension', $watchedExtensions); - }) - ->latest('id') - ->first(['id', 'phone_number', 'target_extension', 'received_at', 'message_text', 'metadata']); - - if (! $latest) { - $this->liveIncomingCall = null; - $this->liveCallCanClickToCall = false; - return; - } - - $phone = $this->normalizePhoneDigits((string) ($latest->phone_number ?? '')); - if ($phone === '') { - $phone = $this->normalizePhoneDigits((string) data_get($latest->metadata, 'smdr.dial_number', '')); - } - - if ($phone === '') { - $this->liveIncomingCall = null; - $this->liveCallCanClickToCall = false; - return; - } - - $rubrica = $this->matchRubricaByPhone($phone); - $soggetto = $this->matchSoggetto($rubrica, $phone); - - $this->liveIncomingCall = [ - 'message_id' => (int) $latest->id, - 'phone' => $phone, - 'received_at' => optional($latest->received_at)->format('d/m/Y H:i:s'), - 'line' => (string) ($latest->message_text ?? ''), - 'target_extension' => (string) ($latest->target_extension ?? ''), - 'rubrica_id' => (int) ($rubrica?->id ?? 0), - 'rubrica_nome' => (string) ($rubrica?->nome_completo ?: $rubrica?->ragione_sociale ?: ''), - 'soggetto_id' => (int) ($soggetto?->id ?? 0), - ]; - - $this->liveCallCanClickToCall = (bool) ($authUser->pbx_click_to_call_enabled ?? false) - && $userExtension !== ''; + $this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($authUser); + $this->liveCallCanClickToCall = (bool) ($this->liveIncomingCall['can_click_to_call'] ?? false); } public function creaPostItDaChiamataInIngresso(): void @@ -322,6 +276,10 @@ public function richiediClickToCallDaInterno(): void public function getLiveRubricaUrl(): ?string { + if (! empty($this->liveIncomingCall['rubrica_url'])) { + return (string) $this->liveIncomingCall['rubrica_url']; + } + $rubricaId = (int) ($this->liveIncomingCall['rubrica_id'] ?? 0); if ($rubricaId <= 0) { return null; @@ -332,6 +290,10 @@ public function getLiveRubricaUrl(): ?string public function getLiveEstrattoContoUrl(): ?string { + if (! empty($this->liveIncomingCall['estratto_conto_url'])) { + return (string) $this->liveIncomingCall['estratto_conto_url']; + } + $soggettoId = (int) ($this->liveIncomingCall['soggetto_id'] ?? 0); if ($soggettoId <= 0) { return null; @@ -345,6 +307,16 @@ public function getRubricaFilamentUrl(): string return route('filament.admin-filament.pages.gescon.anagrafica.rubrica-universale'); } + public function getAdminMobileHubUrl(): string + { + return route('admin.mobile'); + } + + public function getCondominoMobileTicketUrl(): string + { + return route('condomino.tickets.mobile'); + } + public function getDashboardFilamentUrl(): string { return route('filament.admin-filament.pages.dashboard'); @@ -701,16 +673,16 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int } try { - $path = $file->store('ticket-mobile/' . $ticket->id, 'public'); + $stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-mobile/' . $ticket->id); TicketAttachment::query()->create([ 'ticket_id' => (int) $ticket->id, 'ticket_update_id' => null, 'user_id' => $userId, - 'file_path' => $path, - 'original_file_name' => (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : basename($path)), - 'mime_type' => (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream'), - 'size' => (int) (method_exists($file, 'getSize') ? $file->getSize() : 0), + 'file_path' => $stored['path'], + 'original_file_name' => $stored['original_name'], + 'mime_type' => $stored['mime'], + 'size' => $stored['size'], 'description' => $this->resolveAttachmentDescription((int) $index), ]); @@ -781,55 +753,22 @@ private function normalizePhoneDigits(string $value): string return preg_replace('/\D+/', '', $value) ?: ''; } - private function matchRubricaByPhone(string $digits): ?RubricaUniversale + private function applyLiveCallQueryPrefill(): void { - if ($digits === '') { - return null; + $phone = $this->normalizePhoneDigits((string) request()->query('live_phone', '')); + if ($phone === '') { + return; } - $needle = '%' . $digits . '%'; - $tail = strlen($digits) >= 7 ? substr($digits, -7) : $digits; - $tailNeedle = '%' . $tail . '%'; - - return RubricaUniversale::query() - ->where(function ($q) use ($needle, $tailNeedle): void { - $q->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) - ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) - ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) - ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]) - ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]) - ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]); - }) - ->orderByDesc('updated_at') - ->first(); - } - - private function matchSoggetto(?RubricaUniversale $rubrica, string $phone): ?Soggetto - { - if ($rubrica) { - $cf = trim((string) ($rubrica->codice_fiscale ?? '')); - if ($cf !== '') { - $found = Soggetto::query()->where('codice_fiscale', $cf)->first(); - if ($found) { - return $found; - } - } - - $piva = trim((string) ($rubrica->partita_iva ?? '')); - if ($piva !== '') { - $found = Soggetto::query()->where('partita_iva', $piva)->first(); - if ($found) { - return $found; - } - } + if (! $this->liveIncomingCall) { + $this->liveIncomingCall = [ + 'message_id' => (int) request()->query('live_message_id', 0), + 'phone' => $phone, + 'line' => (string) request()->query('live_note', ''), + ]; } - $tail = strlen($phone) >= 7 ? substr($phone, -7) : $phone; - if ($tail === '') { - return null; - } - - return Soggetto::query()->where('telefono', 'like', '%' . $tail . '%')->first(); + $this->prefillTicketFromLiveCall($phone, (string) request()->query('live_note', '')); } public function excerpt(?string $text, int $limit = 160): string diff --git a/app/Filament/Pages/Supporto/TicketScheda.php b/app/Filament/Pages/Supporto/TicketScheda.php index fd5b7e0..43f90d5 100644 --- a/app/Filament/Pages/Supporto/TicketScheda.php +++ b/app/Filament/Pages/Supporto/TicketScheda.php @@ -6,6 +6,7 @@ use App\Models\TicketAttachment; use App\Models\TicketIntervento; use App\Models\User; +use App\Services\Support\TicketAttachmentUploadService; use App\Support\StabileContext; use BackedEnum; use Filament\Notifications\Notification; @@ -218,16 +219,16 @@ public function caricaAllegati(): void continue; } - $path = $file->store('ticket-scheda/' . $ticket->id, 'public'); + $stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-scheda/' . $ticket->id); TicketAttachment::query()->create([ 'ticket_id' => $ticket->id, 'ticket_update_id' => null, 'user_id' => Auth::id(), - 'file_path' => $path, - 'original_file_name' => (string) $file->getClientOriginalName(), - 'mime_type' => (string) $file->getClientMimeType(), - 'size' => (int) $file->getSize(), + 'file_path' => $stored['path'], + 'original_file_name' => $stored['original_name'], + 'mime_type' => $stored['mime'], + 'size' => $stored['size'], 'description' => $this->descrizioneAllegati ?: 'Allegato da scheda ticket', ]); diff --git a/app/Livewire/Filament/TopbarLiveCall.php b/app/Livewire/Filament/TopbarLiveCall.php new file mode 100644 index 0000000..907346b --- /dev/null +++ b/app/Livewire/Filament/TopbarLiveCall.php @@ -0,0 +1,79 @@ +|null */ + public ?array $liveIncomingCall = null; + + public function mount(): void + { + $this->refreshLiveIncomingCall(); + } + + public function refreshLiveIncomingCall(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + $this->liveIncomingCall = null; + return; + } + + $this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($user); + } + + public function createPostIt(): void + { + $user = Auth::user(); + if (! $user instanceof User || ! $this->liveIncomingCall) { + return; + } + + $stabileId = StabileContext::resolveActiveStabileId($user); + if (! $stabileId) { + Notification::make()->title('Seleziona prima uno stabile attivo')->warning()->send(); + return; + } + + ChiamataPostIt::query()->create([ + 'stabile_id' => (int) $stabileId, + 'creato_da_user_id' => (int) $user->id, + 'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null, + 'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''), + 'nome_chiamante' => (string) (($this->liveIncomingCall['rubrica_nome'] ?? '') ?: ('Chiamata ' . ($this->liveIncomingCall['phone'] ?? ''))), + 'direzione' => 'in_arrivo', + 'origine' => 'topbar_live_call', + 'origine_id' => (string) ($this->liveIncomingCall['message_id'] ?? ''), + 'oggetto' => 'Chiamata in arrivo da valutare', + 'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))), + 'priorita' => 'Media', + 'stato' => 'post_it', + 'chiamata_il' => now(), + ]); + + Notification::make()->title('Post-it creato dalla testata')->success()->send(); + } + + public function openTicketMobile() + { + if (! $this->liveIncomingCall || empty($this->liveIncomingCall['ticket_mobile_url'])) { + return null; + } + + return redirect()->to((string) $this->liveIncomingCall['ticket_mobile_url']); + } + + public function render() + { + return view('livewire.filament.topbar-live-call'); + } +} \ No newline at end of file diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index cade183..6f80c47 100644 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -55,6 +55,16 @@ public function panel(Panel $panel): Panel ->visible(function () { $user = Auth::user(); + return $user instanceof \App\Models\User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + }), + NavigationItem::make('Cellulare / WebApp') + ->group('NetGescon') + ->icon('heroicon-o-phone') + ->url(fn() => route('admin.mobile')) + ->visible(function () { + $user = Auth::user(); + return $user instanceof \App\Models\User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); }), diff --git a/app/Services/Cti/LiveIncomingCallService.php b/app/Services/Cti/LiveIncomingCallService.php new file mode 100644 index 0000000..05201b3 --- /dev/null +++ b/app/Services/Cti/LiveIncomingCallService.php @@ -0,0 +1,134 @@ +|null + */ + public function getForUser(User $user): ?array + { + $userExtension = trim((string) ($user->pbx_extension ?? '')); + $watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($user); + $popupRestricted = (bool) ($user->pbx_popup_enabled ?? false) && count($watchedExtensions) > 0; + + $latest = CommunicationMessage::query() + ->whereIn('channel', ['smdr', 'panasonic_csta']) + ->where('direction', 'inbound') + ->whereNotNull('received_at') + ->where('received_at', '>=', now()->subMinutes(8)) + ->when($popupRestricted, function ($query) use ($watchedExtensions): void { + $query->whereIn('target_extension', $watchedExtensions); + }) + ->latest('id') + ->first(['id', 'phone_number', 'target_extension', 'received_at', 'message_text', 'metadata']); + + if (! $latest) { + return null; + } + + $phone = $this->normalizePhoneDigits((string) ($latest->phone_number ?? '')); + if ($phone === '') { + $phone = $this->normalizePhoneDigits((string) data_get($latest->metadata, 'smdr.dial_number', '')); + } + + if ($phone === '') { + return null; + } + + $rubrica = $this->matchRubricaByPhone($phone); + $soggetto = $this->matchSoggetto($rubrica, $phone); + + return [ + 'message_id' => (int) $latest->id, + 'phone' => $phone, + 'received_at' => optional($latest->received_at)->format('d/m/Y H:i:s'), + 'line' => (string) ($latest->message_text ?? ''), + 'target_extension' => (string) ($latest->target_extension ?? ''), + 'rubrica_id' => (int) ($rubrica?->id ?? 0), + 'rubrica_nome' => (string) ($rubrica?->nome_completo ?: $rubrica?->ragione_sociale ?: ''), + 'soggetto_id' => (int) ($soggetto?->id ?? 0), + 'can_click_to_call' => (bool) ($user->pbx_click_to_call_enabled ?? false) && $userExtension !== '', + 'ticket_mobile_url' => TicketMobile::getUrl([ + 'live_phone' => $phone, + 'live_message_id' => (int) $latest->id, + 'live_note' => (string) ($latest->message_text ?? ''), + ], panel: 'admin-filament'), + 'rubrica_url' => (int) ($rubrica?->id ?? 0) > 0 + ? \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => (int) $rubrica->id], panel: 'admin-filament') + : null, + 'estratto_conto_url' => (int) ($soggetto?->id ?? 0) > 0 + ? \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(['record' => (int) $soggetto->id], panel: 'admin-filament') + : null, + 'post_it_redirect_url' => TicketMobile::getUrl([ + 'live_phone' => $phone, + 'live_message_id' => (int) $latest->id, + 'live_note' => (string) ($latest->message_text ?? ''), + 'live_action' => 'post-it', + ], panel: 'admin-filament'), + ]; + } + + private function normalizePhoneDigits(string $value): string + { + return preg_replace('/\D+/', '', $value) ?: ''; + } + + private function matchRubricaByPhone(string $digits): ?RubricaUniversale + { + if ($digits === '') { + return null; + } + + $needle = '%' . $digits . '%'; + $tail = strlen($digits) >= 7 ? substr($digits, -7) : $digits; + $tailNeedle = '%' . $tail . '%'; + + return RubricaUniversale::query() + ->where(function ($query) use ($needle, $tailNeedle): void { + $query->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]); + }) + ->orderByDesc('updated_at') + ->first(); + } + + private function matchSoggetto(?RubricaUniversale $rubrica, string $phone): ?Soggetto + { + if ($rubrica) { + $cf = trim((string) ($rubrica->codice_fiscale ?? '')); + if ($cf !== '') { + $found = Soggetto::query()->where('codice_fiscale', $cf)->first(); + if ($found) { + return $found; + } + } + + $piva = trim((string) ($rubrica->partita_iva ?? '')); + if ($piva !== '') { + $found = Soggetto::query()->where('partita_iva', $piva)->first(); + if ($found) { + return $found; + } + } + } + + $tail = strlen($phone) >= 7 ? substr($phone, -7) : $phone; + if ($tail === '') { + return null; + } + + return Soggetto::query()->where('telefono', 'like', '%' . $tail . '%')->first(); + } +} \ No newline at end of file diff --git a/app/Services/Cti/PbxRoutingService.php b/app/Services/Cti/PbxRoutingService.php index 45509ac..b0c04bd 100644 --- a/app/Services/Cti/PbxRoutingService.php +++ b/app/Services/Cti/PbxRoutingService.php @@ -90,6 +90,7 @@ public function getWatchedExtensionsForUser(User $user): array */ public function getStudioExtensionsForAmministratore(Amministratore $amministratore): array { + $pbx = (array) (($amministratore->impostazioni ?? [])['pbx'] ?? []); $centralino = (array) (($amministratore->impostazioni ?? [])['centralino'] ?? []); $extensions = [ @@ -100,6 +101,29 @@ public function getStudioExtensionsForAmministratore(Amministratore $amministrat $this->normalizeExtension((string) Arr::get($centralino, 'interno_gruppo_notte', '')), ]; + $watchExtensions = preg_split('/\s*,\s*/', (string) ($pbx['watch_extensions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: []; + foreach ($watchExtensions as $extension) { + $extensions[] = $this->normalizeExtension((string) $extension); + } + + foreach ((array) ($pbx['response_groups'] ?? []) as $group) { + if (! is_array($group)) { + continue; + } + + $extensions[] = $this->normalizeExtension((string) ($group['extension'] ?? '')); + $extensions[] = $this->normalizeExtension((string) ($group['target_extension'] ?? '')); + } + + foreach ((array) ($pbx['incoming_lines'] ?? []) as $line) { + if (! is_array($line)) { + continue; + } + + $extensions[] = $this->normalizeExtension((string) ($line['route_group'] ?? '')); + $extensions[] = $this->normalizeExtension((string) ($line['target_extension'] ?? '')); + } + return array_values(array_unique(array_filter($extensions, fn($extension): bool => $extension !== ''))); } @@ -118,6 +142,7 @@ public function findStudioContextByExtension(?string $extension): array } foreach (Amministratore::query()->get(['id', 'impostazioni']) as $amministratore) { + $pbx = (array) (($amministratore->impostazioni ?? [])['pbx'] ?? []); $centralino = (array) (($amministratore->impostazioni ?? [])['centralino'] ?? []); $map = [ 'interno_centralino' => ['studio_role' => 'centralino', 'studio_mode' => null], @@ -138,6 +163,39 @@ public function findStudioContextByExtension(?string $extension): array 'studio_mode' => $context['studio_mode'], ]; } + + foreach ((array) ($pbx['response_groups'] ?? []) as $group) { + if (! is_array($group)) { + continue; + } + + if ($this->normalizeExtension((string) ($group['extension'] ?? '')) !== $normalized) { + continue; + } + + return [ + 'amministratore_id' => (int) $amministratore->id, + 'studio_role' => 'gruppo', + 'studio_mode' => filled($group['mode'] ?? null) ? (string) $group['mode'] : null, + ]; + } + + foreach ((array) ($pbx['incoming_lines'] ?? []) as $line) { + if (! is_array($line)) { + continue; + } + + $routeExtension = $this->normalizeExtension((string) (($line['route_group'] ?? '') !== '' ? $line['route_group'] : ($line['target_extension'] ?? ''))); + if ($routeExtension !== $normalized) { + continue; + } + + return [ + 'amministratore_id' => (int) $amministratore->id, + 'studio_role' => 'linea', + 'studio_mode' => filled($line['label'] ?? null) ? (string) $line['label'] : null, + ]; + } } return [ diff --git a/app/Services/Support/TicketAttachmentUploadService.php b/app/Services/Support/TicketAttachmentUploadService.php new file mode 100644 index 0000000..640cbe4 --- /dev/null +++ b/app/Services/Support/TicketAttachmentUploadService.php @@ -0,0 +1,119 @@ +store($directory, 'public'); + + $originalName = method_exists($file, 'getClientOriginalName') + ? (string) $file->getClientOriginalName() + : basename($path); + + $mime = TicketAttachment::normalizeMimeType( + method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '', + $originalName, + $path, + ); + + if (str_starts_with($mime, 'image/')) { + $this->optimizeImage($path, $mime); + $mime = TicketAttachment::normalizeMimeType($mime, $originalName, $path); + } + + $size = 0; + try { + $size = (int) Storage::disk('public')->size($path); + } catch (\Throwable) { + $size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0); + } + + return [ + 'path' => $path, + 'mime' => $mime, + 'size' => $size, + 'original_name' => $originalName, + ]; + } + + private function optimizeImage(string $path, string $mime): void + { + $absolutePath = Storage::disk('public')->path($path); + if (! is_file($absolutePath) || ! function_exists('getimagesize')) { + return; + } + + $size = @getimagesize($absolutePath); + if (! is_array($size)) { + return; + } + + [$width, $height] = $size; + if ($width <= 0 || $height <= 0) { + return; + } + + $maxWidth = 1600; + $maxHeight = 1600; + $ratio = min($maxWidth / $width, $maxHeight / $height, 1); + $targetW = (int) max(1, round($width * $ratio)); + $targetH = (int) max(1, round($height * $ratio)); + + $source = $this->createImageResource($absolutePath, $mime); + if (! $source) { + return; + } + + $target = imagecreatetruecolor($targetW, $targetH); + if ($target === false) { + imagedestroy($source); + return; + } + + if (in_array($mime, ['image/png', 'image/webp'], true)) { + imagealphablending($target, false); + imagesavealpha($target, true); + $transparent = imagecolorallocatealpha($target, 255, 255, 255, 127); + imagefilledrectangle($target, 0, 0, $targetW, $targetH, $transparent); + } + + imagecopyresampled($target, $source, 0, 0, 0, 0, $targetW, $targetH, $width, $height); + + switch ($mime) { + case 'image/png': + imagepng($target, $absolutePath, 6); + break; + case 'image/webp': + if (function_exists('imagewebp')) { + imagewebp($target, $absolutePath, 82); + } + break; + default: + imagejpeg($target, $absolutePath, 82); + break; + } + + imagedestroy($target); + imagedestroy($source); + } + + private function createImageResource(string $absolutePath, string $mime) + { + return match ($mime) { + 'image/png' => function_exists('imagecreatefrompng') ? @imagecreatefrompng($absolutePath) : false, + 'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($absolutePath) : false, + default => function_exists('imagecreatefromjpeg') ? @imagecreatefromjpeg($absolutePath) : false, + }; + } +} \ No newline at end of file diff --git a/resources/views/filament/components/topbar-context.blade.php b/resources/views/filament/components/topbar-context.blade.php index 4d3ba18..cb27069 100644 --- a/resources/views/filament/components/topbar-context.blade.php +++ b/resources/views/filament/components/topbar-context.blade.php @@ -35,7 +35,7 @@ $haRiscaldamento = (bool) ($stabileAttivo?->riscaldamento_centralizzato ?? false); @endphp -
+
+ +
diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php new file mode 100644 index 0000000..6445787 --- /dev/null +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php @@ -0,0 +1,136 @@ +@php($summary = $this->pbxSummary) + +
+
+
Stato integrazione Panasonic / TAPI
+
Questa sezione tiene insieme configurazione PBX, gruppi di risposta, linee entranti e ultimi eventi CRM letti dal canale Panasonic.
+ +
+
+
Stato
+
{{ $summary['label'] }}
+
{{ $summary['assessment'] }}
+
+
+
Provider
+
{{ $summary['provider'] }}
+
Canale: {{ $summary['channel'] }} · Bridge: {{ $summary['bridge_mode'] }}
+
+
+
Eventi CRM
+
{{ $summary['total_messages'] }}
+
Ultime 24h: {{ $summary['last_24h'] }} · Perse: {{ $summary['missed_calls'] }}
+
+
+
Ultimo evento
+
{{ $summary['last_event_type'] ?: 'Nessun evento' }}
+
{{ $summary['last_event_at'] ?: 'Nessun timestamp disponibile' }}
+
+
+ +
+ Interni osservati: + {{ count($summary['watched_extensions']) > 0 ? implode(', ', $summary['watched_extensions']) : 'nessuno configurato' }} +
+
+ +
+
+
Matrice di instradamento
+
+ + + + + + + + + + + + @foreach($this->pbxRoutingRows as $row) + + + + + + + + @endforeach + +
TipoVoceInterno / numeroTargetNote
{{ $row['type'] }}{{ $row['label'] }}{{ $row['extension'] }}{{ $row['target'] }}{{ $row['note'] }}
+
+
+ +
+
Gruppi e linee configurate
+ +
+
Gruppi di risposta
+
+ @if(count($summary['response_groups']) > 0) + @foreach($summary['response_groups'] as $row) +
{{ $row['extension'] ?? '-' }} · {{ $row['label'] ?? 'Gruppo' }} · fallback {{ $row['target_extension'] ?? '-' }} · modalita {{ $row['mode'] ?? '-' }}
+ @endforeach + @else +
Nessun gruppo aggiuntivo configurato. Restano validi i campi studio 601 e 603.
+ @endif +
+
+ +
+
Linee entranti
+
+ @if(count($summary['incoming_lines']) > 0) + @foreach($summary['incoming_lines'] as $row) +
{{ $row['number'] ?? '-' }} · {{ $row['label'] ?? 'Linea' }} · gruppo {{ $row['route_group'] ?? '-' }} · interno {{ $row['target_extension'] ?? '-' }}
+ @endforeach + @else +
Nessuna linea entrante mappata. Usa questa sezione per separare flussi clienti, fornitori e reperibilita.
+ @endif +
+
+
+
+ +
+
Ultimi eventi letti dal CRM
+
Se qui compaiono record, il bridge Panasonic sta gia portando nel CRM almeno una parte del traffico telefonico.
+ +
+ + + + + + + + + + + + + + + @forelse($this->pbxRecentMessages as $row) + + + + + + + + + + + @empty + + + + @endforelse + +
QuandoEventoDirezioneTelefonoInternoUtenteRuoloStato
{{ $row['received_at'] }}{{ $row['event_type'] }}{{ $row['direction'] }}{{ $row['phone_number'] }}{{ $row['target_extension'] }}{{ $row['user'] }}{{ $row['studio_role'] }}{{ $row['studio_mode'] !== '-' ? ' / ' . $row['studio_mode'] : '' }}{{ $row['status'] }}
Nessun evento Panasonic presente nel CRM per questo amministratore.
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/supporto/ticket-mobile.blade.php b/resources/views/filament/pages/supporto/ticket-mobile.blade.php index dd56bd1..2c7d3fa 100644 --- a/resources/views/filament/pages/supporto/ticket-mobile.blade.php +++ b/resources/views/filament/pages/supporto/ticket-mobile.blade.php @@ -58,6 +58,8 @@
@@ -109,9 +111,19 @@
Nessun contatto trovato con questa ricerca.
@endif -
+
Nuovo ticket rapido
Dopo aver selezionato il chiamante, compila e crea il ticket.
+ @if($errors->any()) +
+
Il ticket non e stato inviato.
+
    + @foreach($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif
+
+
Anteprima immediata sul telefono
+
+ +
+
+ @if(count($this->selectedUploads) > 0)
-
Anteprima allegati selezionati (stile catalogo)
+
Anteprima allegati sincronizzati
@foreach($this->selectedUploads as $upload)
@@ -212,7 +249,10 @@ @endif
- Crea ticket + + Crea ticket + Invio ticket... +
@@ -230,4 +270,28 @@ Suggerimento: aggiungi questa pagina ai preferiti del browser sul telefono per usarla come webapp rapida.
+ + diff --git a/resources/views/livewire/filament/topbar-live-call.blade.php b/resources/views/livewire/filament/topbar-live-call.blade.php new file mode 100644 index 0000000..747ea59 --- /dev/null +++ b/resources/views/livewire/filament/topbar-live-call.blade.php @@ -0,0 +1,35 @@ + \ No newline at end of file