'reporting.browse', 'create' => 'reporting.create', 'update' => 'reporting.update', 'destroy' => 'reporting.destroy', ]; protected $storeRequest = \Illuminate\Http\Request::class; protected $updateRequest = \Illuminate\Http\Request::class; protected $moduleName = 'reporting'; protected $SettingsHelper; protected $UserHelper; public function __construct() { parent::__construct(); $this->SettingsHelper = new SettingsHelper(); $this->UserHelper = new UserHelper(); } /** * GET /api/reporting/outbound * * Returns paginated outbound calls. * * Filters (query params): * - daterange : "YYYY-MM-DD HH:mm:ss - YYYY-MM-DD HH:mm:ss" * - did_ext : Source (DID / trunk) filter * - user_ext : Agent user ID (cnum) * - search : Numeric destination search * - per_page : Items per page (default 10) * - page : Page number */ public function outbound(Request $request) { try { $user = Auth::user(); $conds = $this->SettingsHelper->defaultConds(); if ($request->has('daterange')) { try { $range = explode(' - ', $request->daterange); $conds['from'] = Carbon::createFromFormat('Y-m-d H:i:s', trim($range[0]))->format('Y-m-d H:i:s'); $conds['to'] = Carbon::createFromFormat('Y-m-d H:i:s', trim($range[1]))->format('Y-m-d H:i:s'); } catch (\Exception $e) { $conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s'); $conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s'); } } // --- Same scope logic as OutboundController::outboundDatatable() --- $company_users = $this->UserHelper->getCompanyUsers(); if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) { $company_users = User::whereHas('groups', function ($q) { $q->whereIn('groups.id', auth()->user()->accessibleQueueIds()); }) ->whereDoesntHave('roles', fn($q) => $q->where('name', 'company_manager')) ->visibleInReports() ->get(); } $companyUserIds = $company_users->pluck('id')->map(fn($id) => (string) $id)->toArray(); // Base query — same source & logic as web OutboundController $query = OutboundReport::whereBetween('call_time', [$conds['from'], $conds['to']]) ->whereIn('agent_ext', $companyUserIds); // --- Filters (match web OutboundController) --- if ($request->filled('did_ext')) { $query->where('src', $request->did_ext); } if ($request->filled('user_ext')) { $query->where('agent_ext', $request->user_ext); } if ($request->filled('search') && is_numeric($request->search)) { $query->where('called_number', 'LIKE', '%' . trim($request->search) . '%'); } // Role-based restriction: plain users can only see their own calls if ($user->roles[0]->name === 'user' && $user->access_to_all_entity != '1') { $query->where('agent_ext', (string) $user->id); } $query->orderBy('call_time', 'desc'); $perPage = (int) $request->get('per_page', 10); $data = $query->paginate($perPage); $data->getCollection()->transform(function ($item) { $item->call_time = Carbon::parse($item->call_time)->format('Y-m-d h:i:s A'); $item->total_duration = gmdate('H:i:s', $item->duration); $item->talk_time = gmdate('H:i:s', $item->talktime); $item->ring_time = gmdate('H:i:s', $item->ringtime); $item->dialed_number = $item->called_number; if (Auth::user()->hasPermissionTo('reporting.listen') && !empty($item->linkedid)) { $item->listen_url = url("api/reporting/listen/{$item->linkedid}"); $item->listen_status = $item->done ? 'done' : 'pending'; } else { $item->listen_url = null; $item->listen_status = 'not_available'; } unset($item->src, $item->duration, $item->talktime, $item->ringtime, $item->called_number, $item->done, $item->recordingfile, $item->survey, $item->report_date, $item->callid, $item->id, $item->created_at, $item->updated_at); return $item; }); return $this->respond(true, [ 'data' => $data, 'filters' => [ 'daterange' => $conds['from'] . ' - ' . $conds['to'], 'did_ext' => $request->did_ext ?? null, 'user_ext' => $request->user_ext ?? null, 'search' => $request->search ?? null, ], ], [], 'Outbound calls retrieved successfully'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } /** * POST /api/reporting/outbound/done/{id} * * Marks an outbound call as done (sets userfield = 1 on cdr). * Mirrors OutboundController::outDone(). * * Path Parameters: * - id : The linkedid of the call */ public function outDone($id) { try { // Mark in the reporting table (new canonical store) OutboundReport::where('linkedid', $id)->update(['done' => 1]); // Also update the CDR source of truth for backward compatibility DB::update("UPDATE asteriskcdrdb.cdr SET userfield = 1 WHERE linkedid = ?", [$id]); return $this->respond(true, ['id' => $id], [], 'Outbound call marked as done'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } public function outDist(Request $request) { try { // Define the time range $conds = $this->SettingsHelper->defaultConds(); if ($request->has('daterange')) { $range = explode(' - ', $request->daterange); $conds['from'] = Carbon::parse($range[0])->format('Y-m-d 00:00:00'); $conds['to'] = Carbon::parse($range[1])->format('Y-m-d 23:59:59'); } // Fetch data $data = $this->getOutDistribution(['conds' => $conds]); // Sort data by hour usort($data['hourse_table'], function ($a, $b) { return intval($a['hourse']) - intval($b['hourse']); }); // Prepare response data $response = [ 'months' => [ 'labels' => array_values($data['months_processed']), 'data' => $data['months_table'], 'chart_data' => [ 'answered' => array_values($data['answered_by_month']), 'busy' => array_values($data['busy_by_month']), 'failed' => array_values($data['failed_by_month']), 'unanswered' => array_values($data['unans_by_month']) ] ], 'days' => [ 'labels' => array_values($data['days_processed']), 'data' => $data['days_table'], 'chart_data' => [ 'answered' => array_values($data['answered_by_day']), 'busy' => array_values($data['busy_by_day']), 'failed' => array_values($data['failed_by_day']), 'unanswered' => array_values($data['unans_by_day']) ] ], 'hours' => [ 'labels' => array_values($data['hours_processed']), 'data' => $data['hourse_table'], 'chart_data' => [ 'answered' => array_values($data['answered_by_hour']), 'busy' => array_values($data['busy_by_hour']), 'failed' => array_values($data['failed_by_hour']), 'unanswered' => array_values($data['unans_by_hour']) ] ], 'summary' => [ 'total_answered' => $data['answered'], 'total_unanswered' => $data['unanswered'] ?? 0, 'total_busy' => array_sum($data['busy_by_day'] ?? []), 'total_failed' => array_sum($data['failed_by_day'] ?? []), 'total_calls' => $data['answered'] + ($data['unanswered'] ?? 0), 'success_rate' => $data['answered'] > 0 ? round($data['answered'] / ($data['answered'] + ($data['unanswered'] ?? 1)) * 100, 1) : 0 ], 'filters' => [ 'daterange' => $conds['from'] . ' - ' . $conds['to'] ] ]; return $this->respond(true, $response, [], 'Outbound distribution data retrieved successfully'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Failed to retrieve outbound distribution data', 500); } } private function getOutDistribution($data = array()) { $conds = $data['conds']; $user = Auth::user(); // --- Same scope logic as the Outbound Calls report (web + API) --- $company_users = $this->UserHelper->getCompanyUsers(); if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) { $company_users = User::whereHas('groups', function ($q) { $q->whereIn('groups.id', auth()->user()->accessibleQueueIds()); }) ->whereDoesntHave('roles', fn($q) => $q->where('name', 'company_manager')) ->visibleInReports() ->get(); } $company_users = $company_users->pluck('id')->map(fn($id) => (string) $id)->toArray(); // Data source: outbound_reports (one row per call) — same table and // query logic as the Outbound Calls report, instead of asteriskcdrdb.cdr. // Column aliases keep the historical names so the aggregation loop below // is unchanged: calldate ← call_time, cnum ← agent_ext. $query = DB::table('outbound_reports') ->select( 'call_time as calldate', 'disposition', 'agent_ext as cnum' ) ->whereBetween('call_time', [$conds['from'], $conds['to']]) ->whereIn('agent_ext', $company_users) // Combined category bucket also stays fixed across implementations. ->whereIn('disposition', ['ANSWERED', 'BUSY', 'FAILED', 'NO ANSWER']); // Role-based restriction: same as the Outbound Calls report. if ($user->roles[0]->name === 'user' && $user->access_to_all_entity != '1') { $query->where('agent_ext', (string) $user->id); } $return_array = [ 'days_processed' => [], 'hours_processed' => [], 'months_processed' => [], 'months_table' => [], 'days_table' => [], 'hourse_table' => [], 'answered_by_month' => [], 'answered_by_day' => [], 'answered_by_hour' => [], 'unans_by_month' => [], 'unans_by_day' => [], 'unans_by_hour' => [], 'busy_by_month' => [], 'busy_by_day' => [], 'busy_by_hour' => [], 'failed_by_month' => [], 'failed_by_day' => [], 'failed_by_hour' => [], 'answered' => 0, 'unanswered' => 0 ]; $results = $query->orderBy('call_time', 'ASC')->get(); if ($results->isNotEmpty()) { $countMonths = 0; $pervMonth = null; $countDays = 0; $pervDay = null; foreach ($results as $row) { $time_exploded = explode(" ", $row->calldate); $one_day = trim($time_exploded[0]); // Process days if (!in_array($one_day, $return_array['days_processed'])) { if ($countDays != 0) { $_ans = $return_array['answered_by_day'][$pervDay] ?? 0; $_unans = $return_array['unans_by_day'][$pervDay] ?? 0; $_busy = $return_array['busy_by_day'][$pervDay] ?? 0; $_fail = $return_array['failed_by_day'][$pervDay] ?? 0; $_tot = $_ans + $_unans + $_busy + $_fail; $return_array['days_table'][] = [ 'day' => $pervDay, 'label' => $pervDay, 'answord' => $_ans, 'unans' => $_unans, 'busy' => $_busy, 'failed' => $_fail, 'offered' => $_tot, 'rate' => $_tot > 0 ? round(($_ans / $_tot) * 100, 1) . ' %' : '0 %', ]; } $return_array['days_processed'][] = $one_day; $pervDay = $one_day; $countDays++; } // Process hours $hour_exploded = explode(":", $time_exploded[1]); $one_hour = $hour_exploded[0]; if (!in_array($one_hour, $return_array['hours_processed'])) { $return_array['hours_processed'][] = $one_hour; } // Process months $month_year_label = Carbon::parse($row->calldate)->format('m-Y'); if (!in_array($month_year_label, $return_array['months_processed'])) { if ($countMonths != 0) { $_ans = $return_array['answered_by_month'][$pervMonth] ?? 0; $_unans = $return_array['unans_by_month'][$pervMonth] ?? 0; $_busy = $return_array['busy_by_month'][$pervMonth] ?? 0; $_fail = $return_array['failed_by_month'][$pervMonth] ?? 0; $_tot = $_ans + $_unans + $_busy + $_fail; $return_array['months_table'][] = [ 'month' => $pervMonth, 'label' => $pervMonth, 'answord' => $_ans, 'unans' => $_unans, 'busy' => $_busy, 'failed' => $_fail, 'offered' => $_tot, 'rate' => $_tot > 0 ? round(($_ans / $_tot) * 100, 1) . ' %' : '0 %', ]; } $return_array['months_processed'][] = $month_year_label; $pervMonth = $month_year_label; $countMonths++; } // Process call dispositions if ($row->disposition == 'ANSWERED') { $return_array['answered']++; $return_array['answered_by_day'][$one_day] = ($return_array['answered_by_day'][$one_day] ?? 0) + 1; $return_array['answered_by_hour'][$one_hour] = ($return_array['answered_by_hour'][$one_hour] ?? 0) + 1; $return_array['answered_by_month'][$month_year_label] = ($return_array['answered_by_month'][$month_year_label] ?? 0) + 1; } elseif ($row->disposition == 'BUSY') { $return_array['unanswered']++; $return_array['busy_by_day'][$one_day] = ($return_array['busy_by_day'][$one_day] ?? 0) + 1; $return_array['busy_by_hour'][$one_hour] = ($return_array['busy_by_hour'][$one_hour] ?? 0) + 1; $return_array['busy_by_month'][$month_year_label] = ($return_array['busy_by_month'][$month_year_label] ?? 0) + 1; } elseif ($row->disposition == 'FAILED') { $return_array['unanswered']++; $return_array['failed_by_day'][$one_day] = ($return_array['failed_by_day'][$one_day] ?? 0) + 1; $return_array['failed_by_hour'][$one_hour] = ($return_array['failed_by_hour'][$one_hour] ?? 0) + 1; $return_array['failed_by_month'][$month_year_label] = ($return_array['failed_by_month'][$month_year_label] ?? 0) + 1; } elseif ($row->disposition == 'NO ANSWER') { $return_array['unanswered']++; $return_array['unans_by_day'][$one_day] = ($return_array['unans_by_day'][$one_day] ?? 0) + 1; $return_array['unans_by_hour'][$one_hour] = ($return_array['unans_by_hour'][$one_hour] ?? 0) + 1; $return_array['unans_by_month'][$month_year_label] = ($return_array['unans_by_month'][$month_year_label] ?? 0) + 1; } } // Add last month and day $_ans = $return_array['answered_by_month'][$month_year_label] ?? 0; $_unans = $return_array['unans_by_month'][$month_year_label] ?? 0; $_busy = $return_array['busy_by_month'][$month_year_label] ?? 0; $_fail = $return_array['failed_by_month'][$month_year_label] ?? 0; $_tot = $_ans + $_unans + $_busy + $_fail; $return_array['months_table'][] = [ 'month' => $month_year_label, 'label' => $month_year_label, 'answord' => $_ans, 'unans' => $_unans, 'busy' => $_busy, 'failed' => $_fail, 'offered' => $_tot, 'rate' => $_tot > 0 ? round(($_ans / $_tot) * 100, 1) . ' %' : '0 %', ]; $_ans = $return_array['answered_by_day'][$one_day] ?? 0; $_unans = $return_array['unans_by_day'][$one_day] ?? 0; $_busy = $return_array['busy_by_day'][$one_day] ?? 0; $_fail = $return_array['failed_by_day'][$one_day] ?? 0; $_tot = $_ans + $_unans + $_busy + $_fail; $return_array['days_table'][] = [ 'day' => $one_day, 'label' => $one_day, 'answord' => $_ans, 'unans' => $_unans, 'busy' => $_busy, 'failed' => $_fail, 'offered' => $_tot, 'rate' => $_tot > 0 ? round(($_ans / $_tot) * 100, 1) . ' %' : '0 %', ]; // Process hours table foreach ($return_array['hours_processed'] as $hour) { $_ans = $return_array['answered_by_hour'][$hour] ?? 0; $_unans = $return_array['unans_by_hour'][$hour] ?? 0; $_busy = $return_array['busy_by_hour'][$hour] ?? 0; $_fail = $return_array['failed_by_hour'][$hour] ?? 0; $_tot = $_ans + $_unans + $_busy + $_fail; $return_array['hourse_table'][] = [ 'hourse' => $hour, 'label' => $hour, 'answord' => $_ans, 'unans' => $_unans, 'busy' => $_busy, 'failed' => $_fail, 'offered' => $_tot, 'rate' => $_tot > 0 ? round(($_ans / $_tot) * 100, 1) . ' %' : '0 %', ]; } // Sort hours sort($return_array['hours_processed']); } else { $return_array['answered'] = 0; $return_array['unanswered'] = 0; } return $return_array; } }