countByStatus(Lead::STATUS_NEW); $countContacts = $contactService->countByStatus(Contact::STATUS_NEW); $countOrders = $orderService->countByStatus(Order::STATUS_CREATED); $countInvoices = $invoiceService->countByStatus(Invoice::STATUS_CREATED); $currentCurrency = $request->get('ivseCurrency', config('dashboard.income_vs_expense_default')); $view->with('countContacts', $countContacts); $view->with('countLeads', $countLeads); $view->with('countOrders', $countOrders); $view->with('countInvoices', $countInvoices); $view->with('paymentCurrency', Currency::all()->pluck('code', 'code')); $currency = Currency::whereCode($currentCurrency)->first(); if ($currency) { $currentCurrency = $currency->code; } else { $currentCurrency = config('dashboard.income_vs_expense_default'); } $view->with('currentCurrency', $currentCurrency); $ticketDatatable = new TicketDatatable(); $ticketDatatable->setTableId('TicketDatatable'); $ticketDatatable->setAjaxSource(route('dashboard.new-tickets')); $view->with('leadOverview', new LeadOverview($leadService->groupByStatus())); $view->with('ticketStatusOverview', new TicketOverview($ticketService->groupByStatus())); $view->with('ticketPriorityOverview', new TicketOverview($ticketService->groupByPriority(), '#FF9800')); $view->with('incomeVsExpense', new IncomeVsExpense($paymentsService->incomeVsExpense($currentCurrency))); $view->with('ticketDatatable', $ticketDatatable->html()); $view->with($userDashboardWidgetService->getSettings(Auth::user()->id)); return $view; } /** * @param TicketDatatable $ticketDatatable * @return mixed */ public function getNewTickets(TicketDatatable $ticketDatatable) { $ticketDatatable->addScope(new TicketStatusScope(Ticket::STATUS_NEW)); return $ticketDatatable->render('core::crud.module.modal-datatable'); } public function phoneStable(Request $request){ $user = auth()->user(); $userId = $user->id; $astman = new \App\Http\Controllers\AstmanController(); $myExtensionStatus = $astman->getMyExtensionStatus(); // Check if user has an active session (any status except Offline/Unavailable) if ($myExtensionStatus['status_text'] !== 'Offline' && $myExtensionStatus['status_text'] !== 'Unavailable') { flash("You already have an active session! 🚫 You can't start a second one. For help, feel free to contact technical support.")->error(); return redirect()->to('/dashboard'); } // Shared Softphone configuration resolver. $data = app(SoftphoneConfigResolver::class)->resolveForUser($user); $view = view('dashboard::phone-stable'); $view->with('data', $data); return $view; } public function langJson(){ $lang = [ "create_group" => "Create Group", "add_someone" => "Add Someone", "find_someone" => "Find someone...", "refresh_registration" => "Refresh Registration", "configure_extension" => "Configure Extension", "auto_answer" => "Auto Answer", "do_no_disturb" => "Do No Disturb", "call_waiting" => "Call Waiting", "record_all_calls" => "Record All Calls", "extension_number" => "Extension Number", "email" => "Email", "mobile" => "Mobile", "alternative_contact" => "Alternate Contact", "full_name" => "Full Name", "eg_full_name" => "eg: Keyla James", "title_description" => "Title / Description", "eg_general_manager" => "eg: General Manager", "internal_subscribe_extension" => "Subscribe Extension (Internal)", "eg_internal_subscribe_extension" => "eg: 100 or john", "mobile_number" => "Mobile Number", "eg_mobile_number" => "eg: 44 123-456 7890", "eg_email" => "eg: Keyla.James@raspberrypi.org", "contact_number_1" => "Contact Number 1", "eg_contact_number_1" => "eg: 1 234 567 8901", "contact_number_2" => "Contact Number 2", "eg_contact_number_2" => "eg: 441234567890", "add" => "Add", "cancel" => "Cancel", "save" => "Save", "reload_required" => "Reload Required", "alert_settings" => "In order to apply these settings, the page must reload, OK?", "account" => "Account", "audio_video" => "Audio & Video", "appearance" => "Appearance", "notifications" => "Notifications", "asterisk_server_address" => "Asterisk Server Address", "eg_asterisk_server_address" => "eg: 192.168.1.1 or raspberrypi.local", "websocket_port" => "WebSocket Port", "eg_websocket_port" => "eg: 4443", "websocket_path" => "WebSocket Path", "eg_websocket_path" => "/ws", "sip_username" => "SIP Username", "eg_sip_username" => "eg: webrtc", "sip_password" => "SIP Password", "eg_sip_password" => "eg: 1234", "speaker" => "Speaker", "microphone" => "Microphone", "camera" => "Camera", "frame_rate" => "Frame Rate (per second)", "quality" => "Quality", "image_orientation" => "Image Orientation", "aspect_ratio" => "Aspect Ratio", "preview" => "Preview", "ringtone" => "Ringtone", "ring_device" => "Ring Device", "auto_gain_control" => "Auto Gain Control", "echo_cancellation" => "Echo Cancellation", "noise_suppression" => "Noise Suppression", "enable_onscreen_notifications" => "Enabled Onscreen Notifications", "alert_notification_permission" => "You need to accept the permission request to allow Notifications", "permission" => "Permission", "error" => "Error", "alert_media_devices" => "MediaDevices was null - Check if your connection is secure (HTTPS)", "alert_error_user_media" => "Error getting User Media.", "alert_file_size" => "The file is bigger than 50MB, you cannot upload this file", "alert_single_file" => "Select a single file", "alert_not_found" => "This item was not found", "edit" => "Edit", "welcome" => "Welcome", "accept" => "Accept", "error_user_agant" => "Error creating User Agent", "registered" => "Registered", "registration_failed" => "Registration Failed", "unregistered" => "Unregistered, bye!", "connected_to_web_socket" => "Connected to Web Socket!", "disconnected_from_web_socket" => "Disconnected from Web Socket!", "web_socket_error" => "Web Socket Error", "connecting_to_web_socket" => "Connecting to Web Socket...", "error_connecting_web_socket" => "Error connecting to the server on the WebSocket port", "sending_registration" => "Sending Registration...", "unsubscribing" => "Unsubscribing...", "disconnecting" => "Disconnecting...", "incoming_call" => "Incoming Call", "incoming_call_from" => "Incoming call from:", "answer_call" => "Answer Call", "answer_call_with_video" => "Answer Call with Video", "reject_call" => "Reject Call", "call_failed" => "Call Failed", "alert_no_microphone" => "Sorry, you don't have any Microphone connected to this computer. You cannot receive calls.", "call_in_progress" => "Call in Progress!", "call_rejected" => "Call Rejected", "trying" => "Trying...", "ringing" => "Ringing...", "call_cancelled" => "Call Cancelled", "call_ended" => "Call ended, bye!", "yes" => "Yes", "no" => "No", "receive_kilobits_per_second" => "Receive Kilobits per second", "receive_packets_per_second" => "Receive Packets per second", "receive_packet_loss" => "Receive Packet Loss", "receive_jitter" => "Receive Jitter", "receive_audio_levels" => "Receive Audio Levels", "send_kilobits_per_second" => "Send Kilobits Per Second", "send_packets_per_second" => "Send Packets Per Second", "state_not_online" => "Not online", "state_ready" => "Ready", "state_on_the_phone" => "On the phone", "state_ringing" => "Ringing", "state_on_hold" => "On hold", "state_unavailable" => "Unavailable", "state_unknown" => "Unknown", "alert_empty_text_message" => "Please enter something into the text box provided and click send", "no_message" => "No Message", "message_from" => "Message from", "starting_video_call" => "Starting Video Call...", "call_extension" => "Call Extension", "call_mobile" => "Call Mobile", "call_number" => "Call Number", "call_group" => "Call Group", "starting_audio_call" => "Starting Audio Call...", "call_recording_started" => "Call Recording Started", "call_recording_stopped" => "Call Recording Stopped", "confirm_stop_recording" => "Are you sure you want to stop recording this call?", "stop_recording" => "Stop Recording?", "width" => "Width", "height" => "Height", "extension" => "Extension", "call_blind_transfered" => "Call Blind Transfered", "connecting" => "Connecting...", "attended_transfer_call_started" => "Attended Transfer Call Started...", "attended_transfer_call_cancelled" => "Attended Transfer Call Cancelled", "attended_transfer_complete_accepted" => "Attended Transfer Complete (Accepted)", "attended_transfer_complete" => "Attended Transfer complete", "attended_transfer_call_ended" => "Attended Transfer Call Ended", "attended_transfer_call_rejected" => "Attended Transfer Call Rejected", "attended_transfer_call_terminated" => "Attended Transfer Call Terminated", "conference_call_started" => "Conference Call Started...", "canference_call_cancelled" => "Conference Call Cancelled", "conference_call_in_progress" => "Conference Call In Progress", "conference_call_ended" => "Conference Call Ended", "conference_call_rejected" => "Conference Call Rejected", "conference_call_terminated" => "Conference Call Terminated", "null_session" => "Session Error, Null", "call_on_hold" => "Call on Hold", "send_dtmf" => "Sent DTMF", "switching_video_source" => "Switching video source", "switching_to_canvas" => "Switching to canvas", "switching_to_shared_video" => "Switching to Shared Video", "switching_to_shared_screeen" => "Switching to Shared Screen", "video_disabled" => "Video Disabled", "line" => "Line", "back" => "Back", "audio_call" => "Audio Call", "video_call" => "Video Call", "find_something" => "Find Something", "remove" => "Remove", "present" => "Present", "scratchpad" => "Scratchpad", "screen" => "Screen", "video" => "Video", "blank" => "Blank", "show_key_pad" => "Show Key Pad", "mute" => "Mute", "unmute" => "Unmute", "start_call_recording" => "Start Call Recording", "stop_call_recording" => "Stop Call Recording", "transfer_call" => "Transfer Call", "cancel_transfer" => "Cancel Transfer", "conference_call" => "Conference Call", "cancel_conference" => "Cancel Conference", "hold_call" => "Hold Call", "resume_call" => "Resume Call", "end_call" => "End Call", "search_or_enter_number" => "Search or enter number", "blind_transfer" => "Blind Transfer", "attended_transfer" => "Attended Transfer", "complete_transfer" => "Complete Transfer", "end_transfer_call" => "End Transfer Call", "call" => "Call", "cancel_call" => "Cancel Call", "join_conference_call" => "Join Conference Call", "end_conference_call" => "End Conference Call", "microphone_levels" => "Microphone Levels", "speaker_levels" => "Speaker Levels", "send_statistics" => "Send Statistics", "receive_statistics" => "Receive Statistics", "find_something_in_the_message_stream" => "Find something in the message stream...", "type_your_message_here" => "Type your message here...", "menu" => "Menu", "confirm_remove_buddy" => "This buddy will be removed from your list. Confirm remove?", "remove_buddy" => "Remove Buddy", "read_more" => "Read More", "started" => "Started", "stopped" => "Stopped", "recording_duration" => "Recording Duration", "a_video_call" => "a video call", "an_audio_call" => "an audio call", "you_tried_to_make" => "You tried to make", "you_made" => "You made", "and_spoke_for" => "and spoke for", "you_missed_a_call" => "You missed a call", "you_recieved" => "You received", "second_single" => "second", "seconds_plural" => "seconds", "minute_single" => "minute", "minutes_plural" => "minutes", "hour_single" => "hour", "hours_plural" => "hours", "bytes" => "Bytes", "kb" => "KB", "mb" => "MB", "gb" => "GB", "tb" => "TB", "pb" => "PB", "eb" => "EB", "zb" => "ZB", "yb" => "YB", "call_on_mute" => "Call on Mute", "call_off_mute" => "Call off Mute", "tag_call" => "Tag Call", "clear_flag" => "Clear Flag", "flag_call" => "Flag Call", "edit_comment" => "Edit Comment", "copy_message" => "Copy Message", "quote_message" => "Quote Message", "select_expression" => "Select Expression", "dictate_message" => "Dictate Message", "alert_speech_recognition" => "Your browser does not support this function, sorry", "speech_recognition" => "Speech Recognition", "im_listening" => "I'm listening...", "msg_silence_detection" => "You were quiet for a while so voice recognition turned itself off.", "msg_no_speech" => "No speech was detected. Try again.", "loading" => "Loading...", "select_video" => "Select Video", "ok" => "OK", "device_settings" => "Device Settings", "call_stats" => "Call Stats", "you_received_a_call_from" => "You received a call from", "you_made_a_call_to" => "You made a call to", "you_answered_after" => "You answered after", "they_answered_after" => "They answered after", "with_video" => "with video", "you_started_a_blind_transfer_to" => "You started a blind transfer to", "you_started_an_attended_transfer_to" => "You started an attended transfer to", "the_call_was_completed" => "The call was completed.", "the_call_was_not_completed" => "The call was not completed.", "you_put_the_call_on_mute" => "You put the call on mute.", "you_took_the_call_off_mute" => "You took the call off mute.", "you_put_the_call_on_hold" => "You put the call on hold.", "you_took_the_call_off_hold" => "You took the call off hold.", "you_ended_the_call" => "You ended the call.", "they_ended_the_call" => "They ended the call.", "call_is_being_recorded" => "Call is being recorded.", "now_stopped" => "Now Stopped", "you_started_a_conference_call_to" => "You started a conference call to", "show_call_detail_record" => "Show Call Detail Record", "call_detail_record" => "Call Detail Record", "call_direction" => "Call Direction", "call_date_and_time" => "Call Date & Time", "ring_time" => "Ring Time", "talk_time" => "Talk Time", "call_duration" => "Call Duration", "flagged" => "Flagged", "call_tags" => "Call Tags", "call_notes" => "Call Notes", "activity_timeline" => "Activity Timeline", "call_recordings" => "Call Recordings", "save_as" => "Save As", "right_click_and_select_save_link_as" => "Right click and select Save Link As", "send" => "Send", "set_status" => "Set Status", "default_status" => "(No Status)", "is_typing" => "is typing", "chat_engine" => "Chat Engine", "server_address" => "Server Address", "xmpp_domain" => "Domain", "eg_xmpp_domain" => "e.g. raspberrypi.local", "eg_xmpp_server_address" => "eg: 192.168.1.1 or raspberrypi.local", "allow_calls_on_dnd" => "Allow calls during Do Not Disturb", "basic_extension" => "Basic Extension", "extension_including_xmpp" => "Extension including Message Exchange", "addressbook_contact" => "Address Book Contact", "subscribe_to_dev_state" => "Subscribe to Device State Notifications" ]; return json_encode($lang); } /** * Change agent phone status (pause/unpause) using AMI * OPTIMIZED: Uses Asterisk Manager Interface instead of CLI commands * * @param string $command - 'pause' or 'unpause' * @param string $name - reason for status change * @param int|null $queueId - optional specific queue ID, null = all queues */ public function changePhoneStatus($command, $name, $queueId = null) { $user = Auth::user(); $extension = $user->id; $time = date('Y-m-d H:i:s'); // Use AMI instead of CLI for better performance and control $astman = new \App\Http\Controllers\AstmanController(); // Prepare AMI QueuePause action parameters $params = [ 'Interface' => "Local/{$user->id}@from-queue/n", 'Paused' => ($command === 'pause') ? 'true' : 'false', 'Reason' => $name ]; // If specific queue provided, add it to params (otherwise affects all queues) if ($queueId !== null) { $params['Queue'] = $queueId; } // Execute single AMI request (works across all queues if no queue specified) $response = $astman->doRequest('QueuePause', $params); // Toggle DND at Asterisk level (PJSIP endpoint + FreePBX DB) // Only enabled when the company setting "Pause automatically enables DND" is on. $pauseEnablesDnd = (bool) CompanySettings::get(SettingsHelper::S_COMPANY_SOFTPHONE_PAUSE_ENABLE_DND); if ($pauseEnablesDnd) { $dndState = ($command === 'pause') ? 'on' : 'off'; $dndProcess = new Process(['asterisk', '-rx', "pjsip set dnd {$extension} {$dndState}"]); $dndProcess->setTimeout(5); $dndProcess->run(); // Match FreePBX feature code *78/*79 behavior: // *78 = database put DND ON , *79 = database del DND if ($command === 'pause') { $dndDbProcess = new Process(['asterisk', '-rx', "database put DND {$extension} ON"]); } else { $dndDbProcess = new Process(['asterisk', '-rx', "database del DND {$extension}"]); } $dndDbProcess->setTimeout(5); $dndDbProcess->run(); } // When unpausing, store the unpause timestamp so the Last Call timer resets // to this moment instead of continuing from the pre-pause call. if ($command === 'unpause') { Redis::connection('ami_events')->setex( "agent:unpause_time:{$extension}", 86400, // 1 day now()->timestamp ); } // Update calendar event if ($command === 'unpause') { $this->updateLoginEvent($user); } else { $this->createLoginEvent($user); } // Publish event to Redis for real-time updates in Go service $redis = Redis::connection('ami_events'); $event = [ 'action' => $command === 'pause' ? 'pause' : 'unpause', 'name' => $name, // Ready / Break / etc 'extension' => (string) $extension, 'channel_type' => 'Local', 'ip_address' => request()->ip(), 'datetime' => now()->toDateTimeString(), 'timestamp' => now()->timestamp, ]; $listKey = 'ami:peer_events:list'; $zsetKey = 'ami:peer_events'; $payload = json_encode($event); $redis->lpush($listKey, $payload); $redis->zadd($zsetKey, $event['timestamp'], $payload); // Check if AMI command was successful if ($response && strpos($response, 'Response: Success') !== false) { flash("You are $name now.")->success(); } else { Log::warning("QueuePause AMI command may have failed for user {$user->id}", [ 'command' => $command, 'reason' => $name, 'response' => substr($response ?? 'null', 0, 200) ]); flash("Status change requested. If issues persist, contact support.")->warning(); } return redirect()->to('/reporting'); } public function createLoginEvent($user) { $calendarId = $this->getOrCreateDefaultCalendar(); $event = Event::where('owned_by_id', $user->id)->where('owned_by_type', User::class) ->where('calendar_id', $calendarId) ->where('updated_at','>', Carbon::now()->subHours(8)->format('Y-m-d H:i:s')) ->whereRaw('start_date = DATE_SUB(end_date, INTERVAL 1 SECOND)') ->where('event_status_id', 2) ->where('description', 'PAUSE') ->first(); if(!$event){ try { $start_date = Carbon::now()->format('Y-m-d H:i:s'); $end_date = Carbon::now()->addSecond()->format('Y-m-d H:i:s'); $event = new Event(); $event->name = $user->id . " - " . $user->name; $event->start_date = $start_date; $event->end_date = $end_date; $event->full_day = false; $event->event_color = '#ff9800'; $event->changeOwnerTo($user); $event->calendar_id = $calendarId; $event->event_priority_id = 1; $event->event_status_id = 2; $event->description = 'PAUSE'; $event->company_id = 1; $event->save(); } catch (\Throwable $th) { echo $th; } } } public function updateLoginEvent($user){ $calendar = Calendar::where('name', 'Attendees Calendar') ->first(); $time = date('Y-m-d H:i:s'); try { $event = Event::where('owned_by_id', $user->id)->where('owned_by_type', User::class) ->where('calendar_id', $calendar->id) // ->whereRaw('start_date = DATE_SUB(end_date, INTERVAL 1 SECOND)') // ->where('event_status_id', 2) // ->where('description', 'LogIn') ->orderBy('updated_at', 'DESC') ->first(); $event->end_date = $time; $event->event_color = '#031fa8'; $event->description = 'PAUSE - UNPAUSE'; $event->event_status_id = 4; $event->save(); } catch (\Throwable $th) { // echo $th; } } public function getOrCreateDefaultCalendar() { $calendar = Calendar::where('name', 'Attendees Calendar') ->first(); if (empty($calendar)) { $calendar = new Calendar(); $calendar->name = 'Attendees Calendar'; $calendar->is_public = false; $calendar->default_view = Calendar::MONT_VIEW; $calendar->first_day = Calendar::WEEK_START_AT_MONDAY; $calendar->day_start_at = Calendar::DEFAULT_DAT_START_AT; $calendar->company_id = Landlord::getTenants()->first(); $calendar->save(); } return $calendar->id; } public function getCallerInfo(Request $request){ if($request->callerID){ $callerID = explode(':',$request->callerID); $callerID = end($callerID); $caller = Lead::where('phone','=',$callerID)->first(); return response($caller); } return response(['message' => 'Sorry, resource not found'],401); } public function getMyAgentStatus(Request $request) { $user = Auth::user(); $extension = $user->id; try { $astman = new \App\Http\Controllers\AstmanController(); $response = $astman->doRequest("QueueStatus"); $status = 'Offline'; if (!empty($response)) { $memberBlocks = explode("Event: QueueMember", $response); array_shift($memberBlocks); foreach ($memberBlocks as $block) { // Match the agent's extension in the Interface/Location line if (preg_match('/Local\/(\d+)@from-queue/', $block, $match)) { if ($match[1] == $extension) { // Extract Paused field if (preg_match('/Paused:\s*(\d+)/i', $block, $pausedMatch)) { $status = ($pausedMatch[1] == '1') ? 'Paused' : 'Avail'; } else { $status = 'Avail'; } break; } } } } // Log activity and create/update calendar events similar to changePhoneStatus try { } catch (\Throwable $e) { // ignore logging errors Log::debug('Activity log failed for getMyAgentStatus: ' . $e->getMessage()); } // Create or update pause/unpause events where appropriate if ($status === 'Paused') { $this->createLoginEvent($user); } elseif ($status === 'Avail') { $this->updateLoginEvent($user); } return response()->json(['status' => $status, 'extension' => $extension]); } catch (\Throwable $th) { Log::warning("getMyAgentStatus failed for user {$user->id}: " . $th->getMessage()); return response()->json(['status' => 'Offline', 'extension' => $extension]); } } public function getPhoneStatus(Request $request) { $currentStatus = $request->input('agent_status'); if (!$currentStatus) { return response()->json([ 'status' => [], 'current_stat' => $currentStatus ]); } $statusQuery = DB::table('user_starus')->select('name', 'command', 'color', 'icon'); if ($currentStatus != 'Paused') { $status = $statusQuery->where('command', 'pause')->get(); } else { $status = $statusQuery->where('command', 'unpause')->get(); } return response()->json([ 'status' => $status, 'current_stat' => $currentStatus ]); } /** * GET /dashboard/phone/active-linkedid * * Returns the Asterisk LinkedID of the authenticated agent's current active * call by parsing CoreShowChannels over AMI. * * Used by the softphone to tag forms and UI elements with a stable call * identifier during the call (before CDR is written at hangup). */ public function getActiveLinkedid(Request $request) { $user = Auth::user(); $extension = (string) $user->id; $responseForCall = function (?string $linkedid) use ($user) { $gln = null; if ($linkedid && CompanySettings::get(SettingsHelper::S_COMPANY_GLN_ENABLED, null, $user->company_id)) { $gln = Redis::get('call-gln:' . $linkedid); } return response()->json(['linkedid' => $linkedid, 'gln' => $gln]); }; try { $astman = new \App\Http\Controllers\AstmanController(); $response = $astman->doRequest('CoreShowChannels'); if (empty($response)) { return $responseForCall(null); } // CoreShowChannels emits one "Event: CoreShowChannel" block per channel. // Each block contains a "Linkedid:" field — the canonical call ID. $blocks = explode('Event: CoreShowChannel', $response); array_shift($blocks); // first element is the response header, not a channel foreach ($blocks as $block) { // Match PJSIP/- and PJSIP/99- channels // belonging to this agent. Skip Local/ (queue bridge legs) and SIP spy channels. if (!preg_match('/^\s*Channel:\s*PJSIP\/(?:99)?' . preg_quote($extension, '/') . '-/im', $block)) { continue; } if (preg_match('/^\s*Linkedid:\s*([^\r\n]+)/im', $block, $m)) { return $responseForCall(trim($m[1])); } } return $responseForCall(null); } catch (\Throwable $th) { Log::warning("getActiveLinkedid failed for user {$user->id}: " . $th->getMessage()); return $responseForCall(null); } } public function getSipStatus() { $user = auth()->user(); $companyId = $user->company_id; $wssExt = User::where('users.id', $user->id) ->join('user_extensions', 'users.id', '=', 'user_extensions.user_id') ->where('user_extensions.sip_credentials_enabled', true) ->orderBy('user_extensions.id', 'DESC') ->first(); $globalWssUrl = $companyId ? CompanySettings::get(SettingsHelper::S_PBX_WSS_URL, null, $companyId) : null; if ($wssExt && $wssExt->label) { $extension = (string) $wssExt->label; } elseif (!empty($globalWssUrl)) { $extension = (string) $user->id; } else { $extension = '99' . $user->id; } try { $astman = new \App\Http\Controllers\AstmanController(); $contacts = $astman->PJSIPShowContacts2([$extension]); $registered = !empty(array_filter($contacts, function ($c) use ($extension) { return (string)$c['extension'] === $extension && isset($c['status']) && strtolower($c['status']) !== 'unavailable'; })); } catch (\Exception $e) { Log::error('getSipStatus failed: ' . $e->getMessage()); $registered = false; } return response()->json(['registered' => $registered, 'extension' => $extension]); } public function monitorLog(Request $request) { $extension = $request->input('extension', ''); $agentName = $request->input('agent_name', ''); $mode = $request->input('mode', 'spy'); if (!in_array($mode, ['spy', 'whisper', 'barge'])) { return response()->json(['success' => false], 422); } Log::info('supervisor_monitor', [ 'supervisor_id' => auth()->id(), 'supervisor_name' => auth()->user()->name, 'target_extension' => $extension, 'target_name' => $agentName, 'mode' => $mode, 'ip' => $request->ip(), 'timestamp' => now()->toDateTimeString(), ]); return response()->json(['success' => true]); } public function getSipCredentials() { $user = auth()->user(); $wssExt = User::where('users.id', $user->id) ->join('user_extensions', 'users.id', '=', 'user_extensions.user_id') ->where('user_extensions.sip_credentials_enabled', true) ->orderBy('user_extensions.id', 'DESC') ->first(); $companyId = $user->company_id; $globalWssUrl = $companyId ? CompanySettings::get(SettingsHelper::S_PBX_WSS_URL, null, $companyId) : null; $globalWssPort = $companyId ? CompanySettings::get(SettingsHelper::S_PBX_WSS_PORT, null, $companyId) : null; if ($wssExt && $wssExt->label && $wssExt->sip_domain) { $extension = $wssExt->label; $pbx_url = $wssExt->sip_domain; $ws_port = $wssExt->sip_port ?? 8089; } elseif (!empty($globalWssUrl)) { $extension = $user->id; $pbx_url = $globalWssUrl; $ws_port = !empty($globalWssPort) ? $globalWssPort : 8089; } else { $extension = '99' . $user->id; $pbx_url = url(''); $ws_port = env('WEBSOCKETPORT', 8089); } if (str_contains($pbx_url, '://')) { $pbx_url = explode('://', $pbx_url)[1]; } $sipRow = \DB::table('asterisk.sip')->where(['keyword' => 'secret', 'id' => $extension])->first(); $password = $sipRow ? $sipRow->data : null; return response()->json([ 'wssServer' => $pbx_url, 'port' => (int) $ws_port, 'path' => '/ws', 'sipDomain' => $pbx_url, 'sipUsername' => (string) $extension, 'sipPassword' => $password, 'profileName' => $user->name, ]); } }