SettingsHelper = new SettingsHelper();
$this->UserHelper = new UserHelper();
}
public function outbound(Request $request){
$conds = $this->SettingsHelper->defaultConds();
if ($request->has('daterange')) {
$range = explode(' - ', $request->daterange);
try {
$conds['from'] = Carbon::createFromFormat('Y-m-d H:i:s', $range[0])->format('Y-m-d H:i:s');
$conds['to'] = Carbon::createFromFormat('Y-m-d H:i:s', $range[1])->format('Y-m-d H:i:s');
} catch (\Exception $e) {
// Fallback to default conditions if parsing fails
$conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s');
$conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s');
}
}
// $u = $this->outboundDatatable($request,$conds);
if ($request->ajax() && empty($request->action)) {
return $this->outboundDatatable($request,$conds);
}elseif(!empty($request->action && $request->action == 'excel')){
// Export should go here
// dd($this->outboundDatatable($request));
}
// Same defensive read as the inbound report: the FreePBX `asterisk`
// database is not present/readable on every box.
try {
$did_options = DB::table('asterisk.incoming')
->where('extension', 'like', '002022%')
->pluck('extension', 'extension')
->toArray();
} catch (\Throwable $e) {
$did_options = [];
}
$did_options = [
'dids' => $did_options
];
$agent_names = $this->UserHelper->getCompanyUsers();
$agents_options = [
'Agents' => $agent_names->pluck('name','id')->toArray()
];
$agent_names = $agent_names->pluck('id','name')->toArray();
$view = view('reporting::outbound');
$filter_area = view('reporting::widgets.filter_area');
$filter_area->with('conds',$conds);
$filter_area->with('did_options',$did_options);
$filter_area->with('did_ext',$request->did_ext);
$filter_area->with('agents_options',$agents_options);
$filter_area->with('user_ext',$request->user_ext);
$filter_area->with('submit_route',route('reporting.ccoutbound'));
$view->with('filter_area',$filter_area);
$view->with('conds',$conds);
$view->with('did_ext',$request->did_ext);
$view->with('user_ext',$request->user_ext);
return $view;
}
public function excelReport(Request $request){
$conds = $this->SettingsHelper->defaultConds();
if ($request->has('daterange')) {
$range = explode(' - ', $request->daterange);
try {
$conds['from'] = Carbon::createFromFormat('Y-m-d H:i:s', $range[0])->format('Y-m-d H:i:s');
$conds['to'] = Carbon::createFromFormat('Y-m-d H:i:s', $range[1])->format('Y-m-d H:i:s');
} catch (\Exception $e) {
// Fallback to default conditions if parsing fails
$conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s');
$conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s');
}
}
$user = Auth::user();
// Data source: outbound_reports — the same table and query logic as the
// Outbound Report datatable, instead of asteriskcdrdb.cdr.
$data = OutboundReport::whereBetween('call_time', [$conds['from'], $conds['to']]);
// Scope to users visible to the current authenticated user (same as the report).
$companyUsers = $this->UserHelper->getCompanyUsers();
if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) {
$companyUsers = User::whereHas('groups', function ($q) {
$q->whereIn('groups.id', auth()->user()->accessibleQueueIds());
})
->whereDoesntHave('roles', function ($q) {
$q->where('name', 'company_manager');
})
->visibleInReports()
->get();
}
$companyUserIds = $companyUsers->pluck('id')->map(fn($id) => (string) $id)->toArray();
$data = $data->whereIn('agent_ext', $companyUserIds);
if (!empty($request->did_ext)) {
$data = $data->where('src', $request->did_ext);
}
if (!empty($request->user_ext)) {
$data = $data->where('agent_ext', $request->user_ext);
}
// Same search semantics as the report (numeric search → called_number).
if (!empty($request->search) && is_numeric($request->search)) {
$data = $data->where('called_number', 'LIKE', '%' . trim($request->search) . '%');
}
if ($user->roles[0]->name == 'user' && $user->access_to_all_entity != '1') {
$data = $data->where('agent_ext', (string) $user->id);
}
$data = $data->orderBy('call_time', 'DESC')->get();
$headers = [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition' => 'attachment; filename=users.xlsx',
];
$callback = function () use ($data) {
$excel = new Spreadsheet();
$excel->getActiveSheet()->setCellValue('A1', 'Time');
$excel->getActiveSheet()->setCellValue('B1', 'Ext.');
$excel->getActiveSheet()->setCellValue('C1', 'Agent Name');
$excel->getActiveSheet()->setCellValue('D1', 'Number');
$excel->getActiveSheet()->setCellValue('E1', 'T. Duration');
$excel->getActiveSheet()->setCellValue('F1', 'TalkTime');
$excel->getActiveSheet()->setCellValue('G1', 'RingTime');
$excel->getActiveSheet()->setCellValue('H1', 'Disp.');
$excel->getActiveSheet()->setCellValue('I1', 'Dialed Thru');
$data = $data->map(function ($data) {
return [
date('Y-m-d H:i:s',strtotime($data->call_time)),
$data->agent_ext,
$data->agent_name,
$data->called_number,
gmdate('H:i:s', $data->duration),
gmdate('H:i:s', $data->talktime),
gmdate('H:i:s', $data->ringtime),
$data->disposition,
$data->src,
];
});
$sheet = $excel->getActiveSheet();
// Apply header cell styles
$headerStyle = [
'font' => ['bold' => true],
];
$sheet->getStyle('A1:J1')->applyFromArray($headerStyle);
$sheet->fromArray($data->toArray(), null, 'A2');
$excel = IOFactory::createWriter($excel, 'Xlsx');
$excel->save('php://output');
};
return Response::stream($callback, 200, $headers);
}
public function outboundDatatable($request, $conds)
{
$user = Auth::user();
$canSeeSurvey = $user->hasPermissionTo('reporting.survey_outbound');
// Build base query from the outbound_reports table
$data = OutboundReport::whereBetween('call_time', [$conds['from'], $conds['to']]);
// Scope to users visible to the current authenticated user
$companyUsers = $this->UserHelper->getCompanyUsers();
if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) {
$companyUsers = User::whereHas('groups', function ($q) {
$q->whereIn('groups.id', auth()->user()->accessibleQueueIds());
})
->whereDoesntHave('roles', function ($q) {
$q->where('name', 'company_manager');
})
->visibleInReports()
->get();
}
$companyUserIds = $companyUsers->pluck('id')->map(fn($id) => (string) $id)->toArray();
$data = $data->whereIn('agent_ext', $companyUserIds);
if (!empty($request->did_ext)) {
$data = $data->where('src', $request->did_ext);
}
if (!empty($request->user_ext)) {
$data = $data->where('agent_ext', $request->user_ext);
}
if (!empty($request->search['value']) && is_numeric($request->search['value'])) {
$data = $data->where('called_number', 'LIKE', '%' . trim($request->search['value']) . '%');
}
if ($user->roles[0]->name == 'user' && $user->access_to_all_entity != '1') {
$data = $data->where('agent_ext', (string) $user->id);
}
// The AI analysis state for each row, resolved in the same statement
// (correlated subquery) instead of one query per row.
$data = $data->addSelect([
'ai_analysis_status' => CallAiAnalysis::query()
->select('status')
->whereColumn('call_ai_analyses.linkedid', 'outbound_reports.linkedid')
->orderByDesc('id')
->limit(1),
]);
return Datatables::of($data)
->addIndexColumn()
// Internal alias only: do not ship it to the browser.
->removeColumn('ai_analysis_status')
->addColumn('action', function ($record) {
$btn = '';
if (Auth::user()->hasPermissionTo('reporting.listen')) {
$display_url = route('reporting.listen.page', ['id' => $record->linkedid]);
if (!$record->done) {
$btn = 'Listen';
} else {
$btn = 'Listen';
}
}
// AI summary: Submit while there is no analysis, the stored
// analysis once it exists (a link styled as a button through to
// the analysis page).
$analysis = $this->analysisActionHtml((string) $record->linkedid, $record->ai_analysis_status ?? null, 'outbound');
$action = trim($btn . ' ' . $analysis);
return $action !== '' ? $action : 'Not Avail';
})
->editColumn('call_time', function ($row) {
return date('Y-m-d h:i:s A', strtotime($row->call_time));
})
->editColumn('src', function ($row) {
return $row->src ? str_replace('002022', '022', $row->src) : '';
})
->rawColumns(['action'])
->make(true);
}
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
// (API controller and other consumers still read userfield)
DB::update('UPDATE asteriskcdrdb.cdr SET userfield = 1 WHERE linkedid = ?', [$id]);
return response()->json(['success' => true]);
} catch (\Throwable $th) {
throw $th;
}
}
function outDist(Request $request) {
$conds = $this->SettingsHelper->defaultConds();
if ($request->has('daterange')) {
$range = explode(' - ', $request->daterange);
try {
$conds['from'] = Carbon::createFromFormat('Y-m-d H:i:s', $range[0])->format('Y-m-d H:i:s');
$conds['to'] = Carbon::createFromFormat('Y-m-d H:i:s', $range[1])->format('Y-m-d H:i:s');
} catch (\Exception $e) {
// Fallback to default conditions if parsing fails
$conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s');
$conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s');
}
}
$filter = [
'conds' => $conds,
];
$data = $this->getOutDistribution($filter);
// return $data;
$view = view('reporting::outDist');
// tables
$monthsDatatables = Datatables::of($data['months_table'])
->addIndexColumn()
->addColumn('offered', function($row){
return $row['answord'] + $row['busy'] + $row['failed'] + $row['unans'];
})
->addColumn('rate', function($row){
return round($row['answord']/($row['answord'] + $row['busy'] + $row['failed'] + $row['unans'])*100,1) . ' %';
})
->make(true);
$monthsDatatablesJson = json_encode($monthsDatatables->getData()->data);
$view->with('monthsDatatablesJson',$monthsDatatablesJson);
//
$daysDatatables = Datatables::of($data['days_table'])
->addIndexColumn()
->addColumn('offered', function($row){
return $row['answord'] + $row['busy'] + $row['failed'] + $row['unans'];
})
->addColumn('rate', function($row){
return round($row['answord']/($row['answord'] + $row['busy'] + $row['failed'] + $row['unans'])*100,1) . ' %';
})
->make(true);
$daysDatatablesJson = json_encode($daysDatatables->getData()->data);
$view->with('daysDatatablesJson',$daysDatatablesJson);
//
$hourDatatables = Datatables::of($data['hourse_table'])
->addIndexColumn()
->addColumn('offered', function($row){
return $row['answord'] + $row['busy'] + $row['failed'] + $row['unans'];
})
->addColumn('rate', function($row){
return round($row['answord']/($row['answord'] + $row['busy'] + $row['failed'] + $row['unans'])*100,1) . ' %';
})
->make(true);
$hourDatatablesJson = json_encode($hourDatatables->getData()->data);
$view->with('hourDatatablesJson',$hourDatatablesJson);
// /////////////////////////////////////////////////////
// charts
$view->with('MonthDist', new CallDist([
'type' => 'out',
'data' => $data['months_table'],
]));
$view->with('dayDist', new CallDist([
'type' => 'out',
'data' => $data['days_table'],
]));
$view->with('hourseDist', new CallDist([
'type' => 'out',
'data' => $data['hourse_table'],
]));
////////////////////////////////////////////////////
$filter_area = view('reporting::widgets.filter_area');
$filter_area->with('conds',$conds);
$filter_area->with('submit_route',route('reporting.ccoutDist'));
$view->with('filter_area',$filter_area);
$view->with('data',$data);
// $view->with('queues',$queues);
$view->with('conds',$conds);
$view->with('did_ext',$request->did_ext);
return $view;
}
public 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', function ($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){
// get days_processed
$time_exploded = explode(" ", $row->calldate);
$one_day = trim($time_exploded[0]);
if (!in_array($one_day, $return_array['days_processed'])) {
if ($countDays != 0) {
@$return_array['days_table'][] = [
'day' => $pervDay,
'label' => $pervDay,
'answord' => $return_array['answered_by_day'][$pervDay] ?? 0,
'unans' => $return_array['unans_by_day'][$pervDay] ?? 0,
'failed' => $return_array['failed_by_day'][$pervDay] ?? 0,
'busy' => $return_array['busy_by_day'][$pervDay] ?? 0,
];
}
$return_array['days_processed'][] = $one_day;
$pervDay = $one_day;
$countDays = $countDays + 1;
}
// get hours_processed
$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;
}
// get months_processed
$month_year_label = Carbon::parse($row->calldate)->format('m-Y');
if (!in_array($month_year_label, $return_array['months_processed'])) {
if ($countMonths != 0) {
@$return_array['months_table'][] = [
'month' => $pervMonth,
'label' => $pervMonth,
'answord' => $return_array['answered_by_month'][$pervMonth] ?? 0,
'failed' => $return_array['failed_by_month'][$pervMonth] ?? 0,
'busy' => $return_array['busy_by_month'][$pervMonth] ?? 0,
'unans' => $return_array['unans_by_month'][$pervMonth] ?? 0,
];
}
$return_array['months_processed'][] = $month_year_label;
$pervMonth = $month_year_label;
$countMonths = $countMonths + 1;
}
// Process events
if ($row->disposition == 'FAILED') {
@$return_array['unanswered']++;
@$return_array['failed_by_day'][$one_day]++;
@$return_array['failed_by_hour'][$one_hour]++;
// @$return_array['failed_by_dayname'][date('w', strtotime($row->calldate))]++;
@$return_array['failed_by_month'][$month_year_label]++;
}elseif ($row->disposition == 'BUSY') {
@$return_array['unanswered']++;
@$return_array['busy_by_day'][$one_day]++;
@$return_array['busy_by_hour'][$one_hour]++;
// @$return_array['busy_by_dayname'][date('w', strtotime($row->calldate))]++;
@$return_array['busy_by_month'][$month_year_label]++;
}elseif ($row->disposition == 'NO ANSWER') {
@$return_array['unanswered']++;
@$return_array['unans_by_day'][$one_day]++;
@$return_array['unans_by_hour'][$one_hour]++;
// @$return_array['unans_by_dayname'][date('w', strtotime($row->calldate))]++;
@$return_array['unans_by_month'][$month_year_label]++;
}elseif ($row->disposition == 'ANSWERED') {
@$return_array['answered']++;
@$return_array['answered_by_day'][$one_day]++;
@$return_array['answered_by_hour'][$one_hour]++;
// @$return_array['answered_by_dayname'][date('w', strtotime($row->calldate))]++;
@$return_array['answered_by_month'][$month_year_label]++;
// @$return_array['total_time_by_day'][$one_day] += $row->data2;
// @$return_array['total_hold_by_day'][$one_day] += $row->data1;
// @$return_array['total_time_by_dayname'][date('w', strtotime($row->calldate))] += $row->data2;
// @$return_array['total_hold_by_dayname'][date('w', strtotime($row->calldate))] += $row->data1;
// @$return_array['total_time_by_hour'][$one_hour] += $row->data2;
// @$return_array['total_hold_by_hour'][$one_hour] += $row->data1;
// @$return_array['total_time_by_month'][$month_year_label] += $row->data2;
// @$return_array['total_hold_by_month'][$month_year_label] += $row->data1;
}
}
@$return_array['months_table'][] = [
'month' => $month_year_label,
'label' => $month_year_label,
'answord' => $return_array['answered_by_month'][$pervMonth] ?? 0,
'failed' => $return_array['failed_by_month'][$pervMonth] ?? 0,
'busy' => $return_array['busy_by_month'][$pervMonth] ?? 0,
'unans' => $return_array['unans_by_month'][$pervMonth] ?? 0,
];
@$return_array['days_table'][] = [
'day' => $one_day,
'label' => $one_day,
'answord' => $return_array['answered_by_day'][$one_day] ?? 0,
'busy' => $return_array['busy_by_day'][$one_day] ?? 0,
'failed' => $return_array['failed_by_day'][$one_day] ?? 0,
'unans' => $return_array['unans_by_day'][$one_day] ?? 0,
];
// Sort hours numerically (earliest -> latest) BEFORE building the
// table so the chart and the data table share the exact same order.
sort($return_array['hours_processed'], SORT_NUMERIC);
foreach($return_array['hours_processed'] as $hour){
@$return_array['hourse_table'][] = [
'hourse' => $hour,
'label' => $hour,
'answord' => $return_array['answered_by_hour'][$hour] ?? 0,
'busy' => $return_array['busy_by_hour'][$hour] ?? 0,
'failed' => $return_array['failed_by_hour'][$hour] ?? 0,
'unans' => $return_array['unans_by_hour'][$hour] ?? 0,
];
}
$answered = $return_array['answered'];
$unanswered = $return_array['unanswered'];
if ($unanswered != 0 && $answered != 0) {
$overall_percentage = ($unanswered / $answered) * 100;
} elseif ($unanswered == 0) {
$overall_percentage = 100;
} else {
$overall_percentage = 0;
}
$overall_percentage = number_format($overall_percentage, 2);
$total_calls = $return_array['answered'] + $return_array['unanswered'];
} else {
// No rows returned
$return_array['answered'] = 0;
$return_array['unanswered'] = 0;
}
return $return_array;
}
}