settingsHelper = new SettingsHelper(); $this->_urlraw = env('AMI_SCHEME', 'http://') . env('AMI_HOST', '127.0.0.1') . ':8088/rawman'; $this->_usermanager = env('AMI_USERNAME', 'theAMIManager'); $this->_secretmanager = env('AMI_SECRET', 'DS3joFO49d0DNkkskjngj4j'); $this->_cookiefile = storage_path('ami_cookies/session_' . time()); $this->debugLog('=== OptimizedDashboardController Constructor ==='); $this->debugLog('Cookie file path: ' . $this->_cookiefile); $this->debugLog('AMI URL: ' . $this->_urlraw); // Establish single AMI connection at construction $this->establishAMIConnection(); } /** * Debug logging helper - can be disabled by setting $debugMode = false */ private function debugLog($message, $context = []) { if ($this->debugMode) { Log::info('[OptimizedDashboard] ' . $message, $context); } } /** * Establish persistent AMI connection once */ private function establishAMIConnection() { $this->debugLog('--- Starting AMI Connection Process ---'); try { $this->debugLog('Step 1: Checking if existing AMI connection is alive...'); if (!$this->checkAMIConnection()) { $this->debugLog('Step 1 Result: No existing connection found or ping failed'); $this->debugLog('Step 2: Attempting to login to AMI...'); $this->loginToAMI(); $this->debugLog('Step 2 Result: Login successful'); } else { $this->debugLog('Step 1 Result: Existing connection is alive, reusing it'); } $this->_amiConnected = true; $this->debugLog('--- AMI Connection Established Successfully ---'); } catch (\Exception $e) { $this->debugLog('!!! AMI Connection Failed !!!', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); Log::error('AMI Connection Failed: ' . $e->getMessage()); $this->_amiConnected = false; } } /** * Check if AMI session is still alive */ private function checkAMIConnection() { $this->debugLog(' -> Checking cookie file existence...'); if (!file_exists($this->_cookiefile)) { $this->debugLog(' -> Cookie file does not exist: ' . $this->_cookiefile); return false; } $this->debugLog(' -> Cookie file exists, sending Ping command...'); $query = $this->buildAMIQuery('Ping'); $this->debugLog(' -> Ping URL: ' . $query); $response = $this->executeAMIRequest($query); $this->debugLog(' -> Ping Response: ' . substr($response, 0, 200)); $isSuccess = strpos($response, 'Response: Success') !== false; $this->debugLog(' -> Ping Result: ' . ($isSuccess ? 'SUCCESS' : 'FAILED')); return $isSuccess; } /** * Login to AMI once */ private function loginToAMI() { $this->debugLog(' -> Building Login URL...'); $query = $this->buildAMIQuery('Login', [ 'Username' => $this->_usermanager, 'Secret' => $this->_secretmanager ]); $this->debugLog(' -> Login URL: ' . $query); $this->debugLog(' -> Cookie jar path: ' . $this->_cookiefile); $this->debugLog(' -> Ensuring cookie directory exists...'); // Ensure the cookies directory exists $cookieDir = dirname($this->_cookiefile); if (!is_dir($cookieDir)) { mkdir($cookieDir, 0755, true); $this->debugLog(' -> Created cookie directory: ' . $cookieDir); } else { $this->debugLog(' -> Cookie directory exists: ' . $cookieDir); } $this->debugLog(' -> Initializing cURL session...'); $ch = curl_init($query); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIESESSION, 1); curl_setopt($ch, CURLOPT_COOKIEJAR, $this->_cookiefile); curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 10 second timeout $this->debugLog(' -> Executing Login request...'); $response = curl_exec($ch); $curlError = curl_error($ch); $curlErrno = curl_errno($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $this->debugLog(' -> cURL HTTP Code: ' . $httpCode); if ($curlErrno) { $this->debugLog(' -> cURL Error: [' . $curlErrno . '] ' . $curlError); throw new \Exception('cURL Error: ' . $curlError); } $this->debugLog(' -> Login Response (first 500 chars): ' . substr($response, 0, 500)); if (strpos($response, 'Response: Success') === false) { $this->debugLog(' -> !!! Login Failed - Response does not contain "Response: Success" !!!'); throw new \Exception('AMI Login Failed - Response: ' . substr($response, 0, 200)); } $this->debugLog(' -> Login Response: SUCCESS'); $this->debugLog(' -> Checking if cookie file was created...'); if (file_exists($this->_cookiefile)) { $cookieContent = file_get_contents($this->_cookiefile); $this->debugLog(' -> Cookie file created successfully'); $this->debugLog(' -> Cookie file size: ' . filesize($this->_cookiefile) . ' bytes'); $this->debugLog(' -> Cookie content (first 200 chars): ' . substr($cookieContent, 0, 200)); } else { $this->debugLog(' -> WARNING: Cookie file was NOT created!'); } } /** * Execute AMI request using existing connection */ private function executeAMIRequest($query) { $this->debugLog(' -> Executing AMI request: ' . substr($query, 0, 100)); $this->debugLog(' -> Using cookie file: ' . $this->_cookiefile); $ch = curl_init($query); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIEFILE, $this->_cookiefile); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $response = curl_exec($ch); $curlError = curl_error($ch); $curlErrno = curl_errno($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlErrno) { $this->debugLog(' -> cURL Error during request: [' . $curlErrno . '] ' . $curlError); } $this->debugLog(' -> HTTP Code: ' . $httpCode); $this->debugLog(' -> Response length: ' . strlen($response) . ' bytes'); return $response; } /** * Build AMI query URL */ private function buildAMIQuery($action, $params = []) { $query = $this->_urlraw . "?action=" . $action; foreach ($params as $key => $value) { $query .= "&$key=" . $value; } return str_replace(' ', '%20', $query); } /** * Main endpoint - returns all dashboard metrics in single response * No authentication or company filtering - returns ALL data */ public function getDashboardMetrics() { $this->debugLog('=== getDashboardMetrics() called ==='); $this->debugLog('AMI Connection Status: ' . ($this->_amiConnected ? 'CONNECTED' : 'NOT CONNECTED')); if (!$this->_amiConnected) { $this->debugLog('!!! Returning error - AMI not connected !!!'); return response()->json([ 'error' => 'AMI connection failed', 'debug_info' => 'Check Laravel logs for detailed AMI connection debug information' ], 500); } $cacheKey = "dashboard_metrics_global"; $this->debugLog('Cache key: ' . $cacheKey); $this->debugLog('Cache enabled: ' . ($this->cacheEnabled ? 'YES' : 'NO')); if ($this->cacheEnabled) { $this->debugLog('Checking cache or collecting fresh data...'); return response()->json(Cache::remember($cacheKey, $this->cacheTTL, function() { return $this->collectAllMetrics(); })); } $this->debugLog('Cache disabled, collecting fresh data...'); return response()->json($this->collectAllMetrics()); } /** * Collect all metrics - NO authentication, NO company filtering * Returns data for ALL queues and ALL extensions */ private function collectAllMetrics() { // Get ALL queues and ALL users - no company filtering $allQueues = Group::pluck('id')->toArray(); $allExtensions = User::pluck('id')->toArray(); $totalAgents = count($allExtensions); // Extensions with UCP variants (99 prefix) $extensionsWithUCP = []; foreach ($allExtensions as $ext) { $extensionsWithUCP[] = $ext; $extensionsWithUCP[] = '99' . $ext; } // Get reset time for daily calculations (using default company_id = 1) $resetTime = $this->getResetTimeRange(1); // Collect real-time data via AMI (REUSING SINGLE CONNECTION) $amiData = $this->collectAMIData($allQueues, $extensionsWithUCP); // Collect outbound data from DB (ONLY 1 QUERY - inbound is from AMI now) $outboundData = $this->collectOutboundMetrics($extensionsWithUCP, $resetTime); // Merge and return return [ 'metrics' => [ // Agent Metrics (from AMI) 'agents_online' => $amiData['agents_online'], 'busy_agents' => $amiData['busy_agents'], 'agents_on_pause' => $amiData['agents_on_pause'], 'total_agents' => $totalAgents, // Inbound Metrics (from AMI QueueStatus) 'current_inbound' => $amiData['current_inbound'], 'waiting_calls' => $amiData['waiting_calls'], 'avg_ringtime_in' => $amiData['avg_ringtime_in'], 'avg_talktime_in' => $amiData['avg_talktime_in'], 'missed_calls_in' => $amiData['missed_calls_in'], 'answered_in' => $amiData['answered_in'], 'total_inbound' => $amiData['answered_in'] + $amiData['missed_calls_in'], // Outbound Metrics (from DB - only this needs DB) 'current_outbound' => $amiData['current_outbound'], 'avg_talktime_out' => $outboundData['avg_talktime_out'], 'no_answered_out' => $outboundData['no_answered_out'], 'answered_out' => $outboundData['answered_out'], 'total_outbound' => $outboundData['answered_out'] + $outboundData['no_answered_out'], ], 'queues' => $amiData['queue_breakdown'], 'active_extensions' => $amiData['active_extensions'], 'active_calls' => $amiData['active_calls'], 'timestamp' => now()->toIso8601String() ]; } /** * Collect ALL real-time data using SINGLE AMI connection * Makes 3 AMI commands but reuses same connection */ private function collectAMIData($queues, $extensions) { // Command 1: Get all registered extensions $pjsipResponse = $this->executeAMIRequest( $this->buildAMIQuery('PJSIPShowContacts') ); $registeredExtensions = $this->parsePJSIPContacts($pjsipResponse, $extensions); // Command 2: Get all queue member statuses + QUEUE STATISTICS $queueResponse = $this->executeAMIRequest( $this->buildAMIQuery('QueueStatus') ); $queueData = $this->parseQueueStatus($queueResponse, $extensions, $queues); // Command 3: Get all active channels $channelsResponse = $this->executeAMIRequest( $this->buildAMIQuery('CoreShowChannels') ); $activeChannels = $this->parseCoreShowChannels($channelsResponse, $extensions); // Merge all data and calculate metrics return $this->calculateAMIMetrics( $registeredExtensions, $queueData, $activeChannels, $extensions ); } /** * Parse PJSIPShowContacts response * Returns array of registered extensions with IP, user agent, latency */ private function parsePJSIPContacts($response, $allowedExtensions) { $this->debugLog('--- Parsing PJSIPShowContacts Response ---'); $this->debugLog('Response length: ' . strlen($response) . ' bytes'); $this->debugLog('Allowed extensions count: ' . count($allowedExtensions)); $contacts = []; // Split by "Event: ContactList" to get individual contacts $contactEvents = explode("Event: ContactList", $response); array_shift($contactEvents); // Remove header section $this->debugLog('Found ' . count($contactEvents) . ' contact events'); $alreadyAdded = []; foreach ($contactEvents as $idx => $rawContact) { if (empty(trim($rawContact))) continue; $this->debugLog(' Processing contact #' . ($idx + 1)); $contactLines = explode("\n", trim($rawContact)); // Parse contact fields $extension = ''; $ipAddress = ''; $userAgent = ''; $status = ''; $latency = 0; foreach ($contactLines as $line) { if (strpos($line, 'Endpoint:') !== false) { $extension = trim(explode(":", $line, 2)[1]); } elseif (strpos($line, 'Uri:') !== false) { $uri = trim(explode(":", $line, 2)[1]); // Extract IP from sip:997@10.10.30.4:49368;ob if (preg_match('/sip:[^@]+@([^:;]+)/', $uri, $matches)) { $ipAddress = $matches[1]; } } elseif (strpos($line, 'UserAgent:') !== false) { $userAgent = trim(explode(":", $line, 2)[1]); } elseif (strpos($line, 'Status:') !== false) { $status = trim(explode(":", $line, 2)[1]); } elseif (strpos($line, 'RoundtripUsec:') !== false) { $latency = (int)trim(explode(":", $line, 2)[1]); } } $this->debugLog(' Extension: ' . $extension); $this->debugLog(' Status: ' . $status); $this->debugLog(' IP: ' . $ipAddress); // Skip if empty extension if (empty($extension)) { $this->debugLog(' SKIPPED: Empty extension'); continue; } // Remove UCP prefix if present (99xxx -> xxx) $originalExtension = $extension; if (strlen($extension) >= 6 && substr($extension, 0, 2) == '99') { $extension = substr($extension, 2); } // Skip invalid or already added if (strlen($extension) < 3 || in_array($extension, $alreadyAdded)) { $this->debugLog(' SKIPPED: Too short or already added'); continue; } // Skip special extensions if (substr($extension, 0, 1) == '*') { $this->debugLog(' SKIPPED: Special extension'); continue; } // Convert latency from microseconds to milliseconds if (is_numeric($latency) && $latency > 0) { $latency = ceil($latency / 1000); } else { $latency = 0; } // Replace user agent name if ($userAgent == 'Browser Phone 0.3.8 (SIPJS - 0.20.0)') { $userAgent = 'HSphone v1.3'; } if (preg_match('/Desktop v(\d+(?:\.\d+)*)/i', $userAgent, $version_matches)) { $userAgent = 'Desktop v' . $version_matches[1]; } // Only add if Available (registered) if ($status != 'Unavailable') { $this->debugLog(' ADDED to contacts array'); $contacts[$extension] = [ 'extension' => $extension, 'ip_address' => $ipAddress, 'user_agent' => $userAgent, 'status' => $status, 'latency' => $latency ]; $alreadyAdded[] = $extension; } else { $this->debugLog(' SKIPPED: Status is Unavailable'); } } $this->debugLog('Total contacts parsed: ' . count($contacts)); $this->debugLog('Contact extensions: ' . implode(', ', array_keys($contacts))); return $contacts; } /** * Parse QueueStatus response * Returns queue members and queue statistics */ private function parseQueueStatus($response, $allowedExtensions, $allowedQueues) { $queueMembers = []; $queueStats = []; // Split by QueueParams (queue info) and QueueMember (member info) $queueParamsSections = explode("Event: QueueParams", $response); array_shift($queueParamsSections); // Remove header foreach ($queueParamsSections as $queueSection) { $lines = explode("\n", trim($queueSection)); // Extract queue name from first line $queueLine = ''; foreach ($lines as $line) { if (strpos($line, 'Queue:') !== false) { $queueLine = $line; break; } } if (empty($queueLine)) continue; $queueNameParts = explode(":", $queueLine); $queueName = trim(end($queueNameParts)); // Skip if not in allowed queues if (!in_array($queueName, $allowedQueues)) { continue; } // Extract queue statistics from QueueParams event $holdTime = 0; $talkTime = 0; $completed = 0; $abandoned = 0; $calls = 0; foreach ($lines as $line) { if (strpos($line, 'HoldTime:') !== false) { $holdTime = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'TalkTime:') !== false) { $talkTime = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'Completed:') !== false) { $completed = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'Abandoned:') !== false) { $abandoned = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'Calls:') !== false) { $calls = (int)trim(explode(":", $line)[1]); } } $queueStats[$queueName] = [ 'hold_time' => $holdTime, 'talk_time' => $talkTime, 'completed' => $completed, 'abandoned' => $abandoned, 'waiting_calls' => $calls ]; } // Now parse queue members $memberSections = explode("Event: QueueMember", $response); array_shift($memberSections); // Remove header foreach ($memberSections as $memberSection) { $lines = explode("\n", trim($memberSection)); // Extract extension from Location or Name field $location = ''; $paused = 0; $pausedReason = ''; $callsTaken = 0; $lastCall = 0; $inCall = 0; $status = 0; $lastPause = 0; $loginTime = 0; $wrapupTime = 0; foreach ($lines as $line) { if (strpos($line, 'Location:') !== false) { $location = trim(explode(":", $line, 2)[1]); } if (strpos($line, 'Paused:') !== false) { $paused = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'PausedReason:') !== false) { $pausedReason = trim(explode(":", $line, 2)[1]); } if (strpos($line, 'CallsTaken:') !== false) { $callsTaken = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'LastCall:') !== false) { $lastCall = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'InCall:') !== false) { $inCall = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'Status:') !== false) { $status = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'LastPause:') !== false) { $lastPause = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'LoginTime:') !== false) { $loginTime = (int)trim(explode(":", $line)[1]); } if (strpos($line, 'Wrapuptime:') !== false) { $wrapupTime = (int)trim(explode(":", $line)[1]); } } // Extract extension from Location (Local/1000@from-queue/n) if (preg_match('/Local\/(\d+)@/', $location, $matches)) { $extension = $matches[1]; if (in_array($extension, $allowedExtensions) || in_array('99' . $extension, $allowedExtensions)) { $queueMembers[$extension] = [ 'extension' => $extension, 'paused' => $paused, 'paused_reason' => $pausedReason, 'calls_taken' => $callsTaken, 'last_call' => $lastCall, 'in_call' => $inCall, 'status' => $status, 'last_pause' => $lastPause, 'login_time' => $loginTime, 'wrapup_time' => $wrapupTime ]; } } } return [ 'members' => $queueMembers, 'stats' => $queueStats ]; } /** * Parse CoreShowChannels response * Returns active channels with direction, duration, status */ private function parseCoreShowChannels($response, $allowedExtensions) { $channels = []; $channelSections = explode("Event: CoreShowChannel", $response); array_shift($channelSections); // Remove header array_pop($channelSections); // Remove footer foreach ($channelSections as $channelSection) { $lines = explode("\n", trim($channelSection)); // Extract channel name from line 0 $channelLine = $lines[0] ?? ''; $channelParts = explode("-", $channelLine); $channelId = explode("/", $channelParts[0] ?? ''); $extension = end($channelId); $extension = explode('@', $extension)[0]; // Extract context from line 9 $contextLine = str_replace("Context:", "", $lines[9] ?? ''); $context = trim($contextLine); // Skip unwanted contexts if (in_array($context, ['from-pstn', 'from-trunk', 'func-apply-sipheaders'])) { continue; } // Only process allowed extensions if (!in_array($extension, $allowedExtensions)) { continue; } // Extract other details $duration = str_replace("Duration:", "", $lines[16] ?? ''); $duration = trim($duration); $status = str_replace("ChannelStateDesc:", "", $lines[2] ?? ''); $status = trim($status); $callerIDNum = str_replace("CallerIDNum:", "", $lines[3] ?? ''); $callerIDNum = trim($callerIDNum); $connectedLineNum = str_replace("ConnectedLineNum:", "", $lines[5] ?? ''); $connectedLineNum = trim($connectedLineNum); // Remove prefix if present if (substr($connectedLineNum, 0, 4) == '6103') { $connectedLineNum = substr($connectedLineNum, 4, -1); } // Determine direction $direction = ''; if ($context == 'from-queue') { $direction = 'inbound'; } elseif (in_array($context, ['macro-dialout-trunk', 'trunk-dial-with-exten'])) { $direction = 'outbound'; } if (!empty($direction)) { $channels[] = [ 'channel' => trim($lines[0]), 'extension' => $extension, 'direction' => $direction, 'caller_id' => $callerIDNum, 'connected_number' => $connectedLineNum, 'duration' => $duration, 'status' => $status, 'context' => $context ]; } } return $channels; } /** * Calculate all metrics from AMI data */ private function calculateAMIMetrics($registeredExtensions, $queueData, $activeChannels, $allowedExtensions) { $queueMembers = $queueData['members']; $queueStats = $queueData['stats']; // Calculate agent metrics $agentsOnline = count($registeredExtensions); $busyAgents = 0; $agentsOnPause = 0; foreach ($queueMembers as $member) { if ($member['paused'] == 1) { $agentsOnPause++; } if ($member['in_call'] == 1 || $member['status'] == 2 || $member['status'] == 4) { $busyAgents++; } } // Calculate channel metrics $currentInbound = 0; $currentOutbound = 0; foreach ($activeChannels as $channel) { if ($channel['direction'] == 'inbound') { // Only count as current inbound if call is Up (being handled) if ($channel['status'] == 'Up') { $currentInbound++; } } elseif ($channel['direction'] == 'outbound') { $currentOutbound++; } } // Build per-queue breakdown BEFORE aggregating $queueBreakdown = []; foreach ($queueStats as $queueId => $stats) { // Get queue name from Group model // $queue-> = \Modules\Platform\User\Entities\Group::find($queueId); $queueName = $queueId; // Count current active inbound calls for this queue $currentInboundForQueue = 0; foreach ($activeChannels as $channel) { if ($channel['direction'] == 'inbound' && $channel['status'] == 'Up') { // Check if the channel belongs to this queue by checking connected extensions // This is a simplified approach - you may need to refine based on your channel context $currentInboundForQueue++; } } // Count members in this specific queue $queueMembersCount = 0; $queueMembersOnline = 0; $queueMembersPaused = 0; $queueMembersBusy = 0; foreach ($queueMembers as $member) { // Note: Current structure doesn't store queue_id per member in parseQueueStatus // All members are aggregated. We'd need to enhance parseQueueStatus to track this. // For now, we'll count totals $queueMembersCount++; if ($member['paused'] == 1) { $queueMembersPaused++; } if ($member['in_call'] == 1 || $member['status'] == 2 || $member['status'] == 4) { $queueMembersBusy++; } } $queueBreakdown[] = [ 'queue_id' => $queueId, 'queue_name' => $queueName, // Real-time metrics 'waiting_calls' => $stats['waiting_calls'], 'current_calls' => $currentInboundForQueue, // Active calls being handled // Completed call metrics (from AMI queue statistics) 'completed_calls' => $stats['completed'], 'abandoned_calls' => $stats['abandoned'], 'total_calls' => $stats['completed'] + $stats['abandoned'], // Time metrics 'avg_hold_time' => $this->settingsHelper->format_time($stats['hold_time']), 'avg_hold_time_seconds' => $stats['hold_time'], 'avg_talk_time' => $this->settingsHelper->format_time($stats['talk_time']), 'avg_talk_time_seconds' => $stats['talk_time'], // Answer rate 'answer_rate' => ($stats['completed'] + $stats['abandoned']) > 0 ? round(($stats['completed'] / ($stats['completed'] + $stats['abandoned'])) * 100, 2) : 0, // Member counts (Note: currently aggregated across all queues) // To get accurate per-queue counts, we'd need to enhance parseQueueStatus 'total_members' => $queueMembersCount, ]; } // Calculate inbound statistics from queue stats (accumulated from all queues) $totalCompleted = 0; $totalAbandoned = 0; $totalHoldTime = 0; $totalTalkTime = 0; $totalWaitingCalls = 0; $queueCount = 0; foreach ($queueStats as $stats) { $totalCompleted += $stats['completed']; $totalAbandoned += $stats['abandoned']; $totalHoldTime += $stats['hold_time']; $totalTalkTime += $stats['talk_time']; $totalWaitingCalls += $stats['waiting_calls']; $queueCount++; } // Calculate averages $avgRingTime = $queueCount > 0 ? floor($totalHoldTime / $queueCount) : 0; $avgTalkTime = $queueCount > 0 ? floor($totalTalkTime / $queueCount) : 0; // Build active extensions list with merged data $activeExtensionsList = []; foreach ($registeredExtensions as $ext => $contact) { $queueMember = $queueMembers[$ext] ?? null; // Determine status based on queue member status code // Status codes: 0=Unknown, 1=Not in use, 2=In use, 4=Busy, 5=Unavailable, 8=Ringing $status = 'Avail'; $pauseReason = ''; $pauseDuration = 0; $currentCallDuration = null; $callsTaken = 0; $lastCall = 0; $loginTime = 0; $wrapupTime = 0; if ($queueMember) { $callsTaken = $queueMember['calls_taken']; $lastCall = $queueMember['last_call']; $loginTime = $queueMember['login_time']; $wrapupTime = $queueMember['wrapup_time']; // Check pause status first if ($queueMember['paused'] == 1) { $status = 'Paused'; $pauseReason = $queueMember['paused_reason']; $pauseDuration = $queueMember['last_pause'] > 0 ? (time() - $queueMember['last_pause']) : 0; } // Then check queue member status elseif ($queueMember['status'] == 2 || $queueMember['status'] == 4) { // Status 2 = In use, Status 4 = Busy $status = 'Busy'; } elseif ($queueMember['status'] == 8) { // Status 8 = Ringing $status = 'Ringing'; } elseif ($queueMember['status'] == 1) { // Status 1 = Not in use (Available) $status = 'Avail'; } elseif ($queueMember['status'] == 5 || $queueMember['status'] == 0) { // Status 5 = Unavailable, Status 0 = Unknown $status = 'Offline'; } } // Override with active call information if present foreach ($activeChannels as $channel) { if ($channel['extension'] == $ext) { if ($channel['status'] == 'Ringing') { $status = 'Ringing'; } elseif ($channel['status'] == 'Up') { $status = $channel['direction'] == 'inbound' ? 'In Call' : 'Out Call'; $currentCallDuration = $channel['duration']; } break; } } // Get user name $user = User::find($ext); $name = $user ? $user->full_name : "Extension {$ext}"; $extData = [ 'extension' => $ext, 'name' => $name, 'status' => $status, 'ip_address' => $contact['ip_address'], 'user_agent' => $contact['user_agent'], 'latency' => $contact['latency'], 'calls_taken' => $callsTaken, 'last_call' => $lastCall > 0 ? date('Y-m-d H:i:s', $lastCall) : null, 'login_time' => $loginTime > 0 ? date('Y-m-d H:i:s', $loginTime) : null, 'wrapup_time' => $wrapupTime ]; if ($status == 'Paused') { $extData['pause_reason'] = $pauseReason; $extData['pause_duration'] = $pauseDuration; } if ($currentCallDuration !== null) { $extData['current_call_duration'] = $currentCallDuration; } $activeExtensionsList[] = $extData; } return [ 'agents_online' => $agentsOnline, 'busy_agents' => $busyAgents, 'agents_on_pause' => $agentsOnPause, 'current_inbound' => $currentInbound, 'current_outbound' => $currentOutbound, 'waiting_calls' => $totalWaitingCalls, 'avg_ringtime_in' => $this->settingsHelper->format_time($avgRingTime), 'avg_talktime_in' => $this->settingsHelper->format_time($avgTalkTime), 'answered_in' => $totalCompleted, 'missed_calls_in' => $totalAbandoned, 'queue_breakdown' => $queueBreakdown, 'active_extensions' => $activeExtensionsList, 'active_calls' => $activeChannels ]; } /** * Only 1 database query - for outbound statistics */ private function collectOutboundMetrics($extensions, $resetTime) { $outboundStats = DB::table('asteriskcdrdb.cdr') ->selectRaw(' COUNT(*) as total_calls, SUM(IF(disposition = "ANSWERED", 1, 0)) as answered, COUNT(*) - SUM(IF(disposition = "ANSWERED", 1, 0)) as not_answered, AVG(CASE WHEN disposition = "ANSWERED" THEN billsec ELSE NULL END) as avg_billsec ') ->whereBetween('calldate', [$resetTime['start'], $resetTime['end']]) ->where('dcontext', 'from-internal') ->where('lastapp', 'Dial') ->whereIn('cnum', $extensions) ->first(); return [ 'avg_talktime_out' => $this->settingsHelper->format_time($outboundStats->avg_billsec ?? 0), 'no_answered_out' => $outboundStats->not_answered ?? 0, 'answered_out' => $outboundStats->answered ?? 0, ]; } /** * Get reset time range for daily calculations */ private function getResetTimeRange($companyId) { $resetTime = CompanySettings::get('dashboard_reset_time', '00:00'); $resetParts = explode(':', $resetTime); $resetHour = isset($resetParts[0]) ? (int)$resetParts[0] : 0; $resetMinute = isset($resetParts[1]) ? (int)$resetParts[1] : 0; $now = Carbon::now(); if ($now->hour < $resetHour || ($now->hour == $resetHour && $now->minute < $resetMinute)) { $startTime = Carbon::yesterday()->setHour($resetHour)->setMinute($resetMinute)->setSecond(0); } else { $startTime = Carbon::today()->setHour($resetHour)->setMinute($resetMinute)->setSecond(0); } $endTime = Carbon::now(); return [ 'start' => $startTime, 'end' => $endTime ]; } }