SettingsHelper = new SettingsHelper();
}
// public function missed(Request $request)
// {
// $user = Auth::user();
// $company_groups = Group::where('company_id',$user->company->id ?? 1)->pluck('id');
// // Set default time range
// $conds = $this->SettingsHelper->defaultConds();
// // Process date range from the request
// if ($request->has('daterange')) {
// try {
// $range = explode(' - ', $request->daterange);
// $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) {
// $conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s');
// $conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s');
// }
// }
// if ($request->ajax()) {
// // Step 1: Get all callids with ABANDON or EXITWITHTIMEOUT events
// $missedCallIds = DB::table('asteriskcdrdb.queuelog')
// ->select('callid')
// ->whereIn('event', ['ABANDON', 'EXITWITHTIMEOUT'])
// ->whereBetween('time', [$conds['from'], $conds['to']])
// ->whereIn('queuename', $company_groups)
// ->whereNotIn('callid', function($q) {
// $q->select('callid')
// ->from('asteriskcdrdb.queuelog')
// ->whereIn('event', ['COMPLETECALLER', 'COMPLETEAGENT']);
// })
// ->groupBy('callid')
// ->pluck('callid');
// if ($missedCallIds->isEmpty()) {
// return Datatables::of(collect())->make(true);
// }
// // Step 2: First ENTERQUEUE per call
// $firstEnterQueue = DB::table('asteriskcdrdb.queuelog')
// ->select('callid', DB::raw('MIN(id) as first_id'))
// ->whereIn('callid', $missedCallIds)
// ->where('event', 'ENTERQUEUE')
// ->groupBy('callid');
// // Step 3: Total duration per call (sum of data3)
// $totalDurations = DB::table('asteriskcdrdb.queuelog')
// ->select('callid', DB::raw('SUM(CAST(data3 AS UNSIGNED)) as total_duration'))
// ->whereIn('callid', $missedCallIds)
// ->whereNotNull('data3')
// ->groupBy('callid');
// // Step 4: Main query
// $data = DB::table('asteriskcdrdb.queuelog as q')
// ->select([
// 'q.id',
// 'q.time',
// 'q.callid',
// 'q.queuename',
// 'qc.descr',
// 'q.agent',
// 'ab.event',
// 'ab.status',
// 'durations.total_duration as duration',
// 'q.data1',
// 'q.data2 as numder',
// 'q.data3',
// 'q.data5'
// ])
// ->join('asterisk.queues_config as qc', 'qc.extension', '=', 'q.queuename')
// ->joinSub($firstEnterQueue, 'feq', function ($join) {
// $join->on('q.id', '=', 'feq.first_id');
// })
// ->leftJoin(DB::raw('(
// SELECT q1.callid, q1.event, q1.data3, q1.status
// FROM asteriskcdrdb.queuelog q1
// JOIN (
// SELECT callid, MAX(id) as max_id
// FROM asteriskcdrdb.queuelog
// WHERE event IN (\'ABANDON\', \'EXITWITHTIMEOUT\')
// GROUP BY callid
// ) q2 ON q1.callid = q2.callid AND q1.id = q2.max_id
// ) as ab'), 'ab.callid', '=', 'q.callid')
// ->leftJoinSub($totalDurations, 'durations', function ($join) {
// $join->on('durations.callid', '=', 'q.callid');
// })
// ->whereIn('q.callid', $missedCallIds)
// ->orderBy('q.callid');
// // Apply user access restrictions
// if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) {
// $data = $data->whereIn('q.queuename', $user->groups->pluck('id')->toArray());
// }
// return Datatables::of($data)
// ->addIndexColumn()
// ->addColumn('action', function($row){
// if (empty($row->data5)) {
// $btn = 'Missed';
// } else {
// $user = User::find($row->data5);
// if ($user) {
// $btn = 'Done by '.$user->name.'';
// } else {
// $btn = 'Done';
// }
// }
// return $btn;
// })
// ->addColumn('ring_details', function($row){
// return '';
// })
// ->editColumn('time', function($row) {
// return Carbon::parse($row->time)->format('Y-m-d h:i:s A');
// })
// ->rawColumns(['action', 'ring_details'])
// ->make(true);
// }
// $agent_names = User::pluck('name','id')->toArray();
// // Prepare and display the interface for non-AJAX requests
// $filter_area = view('reporting::widgets.filter_area', [
// 'conds' => $conds,
// 'submit_route' => route('reporting.missed')
// ]);
// return view('reporting::missed_calls', [
// 'filter_area' => $filter_area,
// 'conds' => $conds,
// 'agent_names' => $agent_names,
// ]);
// }
public function missed(Request $request)
{
$user = Auth::user();
$company_groups = $user->accessibleQueueIds();
// Set default time range
$conds = $this->SettingsHelper->defaultConds();
// Process date range from the request
if ($request->has('daterange')) {
try {
$range = explode(' - ', $request->daterange);
$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) {
$conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s');
$conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s');
}
}
if ($request->ajax()) {
$data = InboundReport::whereBetween('call_time', [$conds['from'], $conds['to']])
->where('call_status', 'missed')
->whereIn('enter_queuename', $company_groups);
// Apply filters
if (!empty($request->search['value'])) {
$searchValue = strtolower(trim($request->search['value']));
$data = $data->where(function ($query) use ($searchValue) {
$query->orWhere(DB::raw('LOWER(queue_description)'), 'LIKE', '%' . $searchValue . '%')
->orWhere(DB::raw('LOWER(caller_number)'), 'LIKE', '%' . $searchValue . '%')
->orWhere(DB::raw('LOWER(did)'), 'LIKE', '%' . $searchValue . '%');
});
}
// Apply user access restrictions
if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) {
$data = $data->whereIn('enter_queuename', $user->accessibleQueueIds());
}
return Datatables::of($data)
->addIndexColumn()
->addColumn('action', function ($row) use ($user) {
if (empty($row->data5)) {
$btn = 'Missed';
} else {
$user = User::find($row->data5);
if ($user) {
$btn = 'Done by ' . $user->name . '';
} else {
$btn = 'Done';
}
}
return $btn;
})
->addColumn('ring_details', function ($row) {
return '';
})
->editColumn('call_time', function ($row) {
return Carbon::parse($row->call_time)->format('Y-m-d h:i:s A');
})
->rawColumns(['action', 'ring_details'])
->make(true);
}
$agent_names = User::pluck('name', 'id')->toArray();
// Prepare and display the interface for non-AJAX requests
$filter_area = view('reporting::widgets.filter_area', [
'conds' => $conds,
'submit_route' => route('reporting.missed')
]);
return view('reporting::missed_calls', [
'filter_area' => $filter_area,
'conds' => $conds,
'agent_names' => $agent_names,
]);
}
/**
* Helper function to format seconds to HH:MM:SS
*/
private function formatSeconds($seconds)
{
if (!$seconds) return '00:00:00';
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
$secs = $seconds % 60;
return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs);
}
public function getRingDetails($callid)
{
// 1. Resolve the missed call from inbound_reports
$missedCall = InboundReport::where('callid', $callid)
->whereIn('hangup_type', ['ABANDON', 'EXITWITHTIMEOUT'])
->first();
if (!$missedCall) {
return response()->json([
'ring_details' => [],
'inbound_calls' => [],
'outbound_calls' => [],
'ring_details_count' => 0,
'inbound_calls_count' => 0,
'outbound_calls_count' => 0,
'active_missed_calls' => [],
'active_missed_calls_count' => 0,
]);
}
$startTime = Carbon::parse($missedCall->call_time)->format('Y-m-d H:i:s');
$endTime = Carbon::parse($missedCall->call_time)->addSeconds($missedCall->ringtime ?? 0)->format('Y-m-d H:i:s');
// 2. Per-agent ring attempt details for the main call (granular queuelog data —
// no equivalent stored in inbound_reports, so queuelog is still used here)
$ringDetails = DB::table('asteriskcdrdb.queuelog')
->select('agent', 'event', 'data1', 'time')
->where('callid', $callid)
->whereIn('event', ['RINGNOANSWER', 'RINGCANCELED'])
->where('data1', '>', 1000)
->orderBy('time', 'asc')
->get();
// 3. Concurrent missed calls — from inbound_reports
$activeMissedCalls = InboundReport::whereIn('hangup_type', ['ABANDON', 'EXITWITHTIMEOUT'])
->where('callid', '!=', $callid)
->where('call_time', '>=', $startTime)
->where('call_time', '<=', $endTime)
->orderBy('call_time', 'asc')
->get()
->map(fn($r) => [
'agent' => $r->agent,
'event' => $r->hangup_type,
'data1' => ($r->ringtime ?? 0) * 1000, // match ms format expected by frontend
'time' => Carbon::parse($r->call_time)->format('Y-m-d H:i:s'),
]);
// 4. Concurrent answered inbound calls — from inbound_reports
$inboundCalls = InboundReport::whereIn('hangup_type', ['COMPLETECALLER', 'COMPLETEAGENT'])
->where('call_time', '<=', $endTime)
->whereRaw('DATE_ADD(call_time, INTERVAL (ringtime + talktime) SECOND) >= ?', [$startTime])
->orderBy('call_time', 'asc')
->get()
->map(fn($r) => [
'enter_time' => Carbon::parse($r->call_time)->format('Y-m-d H:i:s'),
'agent' => $r->agent,
'src' => $r->caller_number,
'did' => $r->did,
'total_duration' => ($r->ringtime ?? 0) + ($r->talktime ?? 0),
]);
// 5. Concurrent outbound calls — from outbound_reports
$outboundCalls = OutboundReport::where('call_time', '<=', $endTime)
->whereRaw('DATE_ADD(call_time, INTERVAL duration SECOND) >= ?', [$startTime])
->whereIn('disposition', ['ANSWERED', 'BUSY', 'NO ANSWER', 'FAILED'])
->orderBy('call_time', 'asc')
->get()
->map(fn($r) => [
'calldate' => Carbon::parse($r->call_time)->format('Y-m-d H:i:s'),
'agent_name' => $r->agent_name,
'dst' => $r->called_number,
'src' => $r->src,
'duration' => $r->duration,
'disposition' => $r->disposition,
]);
return response()->json([
'ring_details' => $ringDetails,
'inbound_calls' => $inboundCalls,
'outbound_calls' => $outboundCalls,
'ring_details_count' => count($ringDetails),
'inbound_calls_count' => count($inboundCalls),
'outbound_calls_count' => count($outboundCalls),
'active_missed_calls' => $activeMissedCalls,
'active_missed_calls_count' => count($activeMissedCalls),
]);
}
function missedDone($num)
{
try {
$userId = Auth::id();
/*
* Normalize the input.
*
* Examples:
* +201002601172
* 201002601172
* 00201002601172
* 01002601172
* +20 100 260 1172
* 010-026-01172
*/
$originalNum = $num;
// Remove spaces, -, (, ), etc. but keep +
$num = preg_replace('/[^0-9+]/', '', $num);
$numbers = [];
/*
* Add original format.
*/
if (!empty($num)) {
$numbers[] = $num;
}
/*
* Remove + for processing.
*
* +201002601172
* ↓
* 201002601172
*/
$cleanNum = ltrim($num, '+');
/*
* ==========================================================
* CASE 1:
* 0020XXXXXXXXXX
*
* Example:
* 00201002601172
*
* Generate:
* 00201002601172
* 201002601172
* +201002601172
* 01002601172
* ==========================================================
*/
if (str_starts_with($cleanNum, '0020')) {
$international = substr($cleanNum, 2);
$numbers[] = $international;
$numbers[] = '+' . $international;
// 201002601172 -> 01002601172
$numbers[] = '0' . substr($international, 2);
}
/*
* ==========================================================
* CASE 2:
* +20XXXXXXXXXX
* 20XXXXXXXXXX
*
* Example:
* +201002601172
* 201002601172
*
* Generate:
* +201002601172
* 201002601172
* 00201002601172
* 01002601172
* ==========================================================
*/ elseif (str_starts_with($cleanNum, '20')) {
$international = $cleanNum;
$numbers[] = $international;
$numbers[] = '+' . $international;
$numbers[] = '00' . $international;
// 201002601172 -> 01002601172
$numbers[] = '0' . substr($international, 2);
}
/*
* ==========================================================
* CASE 3:
* Egyptian local number
*
* Example:
* 01002601172
*
* Generate:
* 01002601172
* 201002601172
* +201002601172
* 00201002601172
* ==========================================================
*/ elseif (preg_match('/^01[0-9]{9}$/', $cleanNum)) {
$international = '20' . substr($cleanNum, 1);
$numbers[] = $cleanNum;
$numbers[] = $international;
$numbers[] = '+' . $international;
$numbers[] = '00' . $international;
}
/*
* Remove duplicate values and empty values.
*/
$numbers = array_values(
array_unique(
array_filter($numbers)
)
);
/*
* Log the generated formats.
*/
Log::info('missedDone phone formats', [
'original' => $originalNum,
'clean' => $cleanNum,
'numbers' => $numbers,
'userId' => $userId,
]);
/*
* ==========================================================
* QUEUELOG
* ==========================================================
*/
$placeholders = implode(
',',
array_fill(0, count($numbers), '?')
);
$sql1 = "UPDATE asteriskcdrdb.queuelog
SET data5 = ?
WHERE event = 'ENTERQUEUE'
AND data2 IN ($placeholders)
AND (data5 IS NULL OR data5 = '')";
$bindings1 = array_merge(
[$userId],
$numbers
);
Log::info('missedDone queuelog SQL', [
'sql' => $sql1,
'bindings' => $bindings1,
]);
DB::update($sql1, $bindings1);
/*
* ==========================================================
* INBOUND REPORTS
* ==========================================================
*/
$sql2 = "UPDATE inbound_reports
SET data5 = ?
WHERE event IN ('ABANDON', 'EXITWITHTIMEOUT')
AND caller_number IN ($placeholders)
AND (data5 IS NULL OR data5 = '')";
$bindings2 = array_merge(
[$userId],
$numbers
);
Log::info('missedDone inbound_reports SQL', [
'sql' => $sql2,
'bindings' => $bindings2,
]);
DB::update($sql2, $bindings2);
return true;
} catch (\Throwable $th) {
throw $th;
}
}
public function callStats(Request $request)
{
$user = Auth::user();
$company_groups = collect($user->accessibleQueueIds());
// Default time range
$conds = $this->SettingsHelper->defaultConds();
if ($request->has('daterange')) {
try {
$range = explode(' - ', $request->daterange);
$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) {
$conds['from'] = Carbon::now()->startOfDay()->format('Y-m-d H:i:s');
$conds['to'] = Carbon::now()->endOfDay()->format('Y-m-d H:i:s');
}
}
// Base query against inbound_reports — one row per missed call, already filtered
$baseQuery = InboundReport::whereBetween('call_time', [$conds['from'], $conds['to']])
->whereIn('hangup_type', ['ABANDON', 'EXITWITHTIMEOUT'])
->whereIn('enter_queuename', $company_groups->toArray());
if ($user->access_to_all_entity != '1' && $user->groups->isNotEmpty()) {
$baseQuery->whereIn('enter_queuename', $user->accessibleQueueIds());
}
// Status counts (ABANDON vs EXITWITHTIMEOUT)
$statusCounts = (clone $baseQuery)
->selectRaw('hangup_type as status, COUNT(*) as count')
->groupBy('hangup_type')
->orderByRaw('COUNT(*) DESC')
->get();
// Reason counts (the call_status / reason field per missed call)
$reasonCounts = (clone $baseQuery)
->selectRaw('COALESCE(NULLIF(status, \'\'), \'Unknown\') as status, COUNT(*) as count')
->groupBy('status')
->orderByRaw('COUNT(*) DESC')
->get();
$statusData = [
'labels' => $statusCounts->pluck('status')->toArray(),
'counts' => $statusCounts->pluck('count')->toArray()
];
$reasonData = [
'labels' => $reasonCounts->map(function ($item) {
return $item->status ?? 'Unknown';
})->toArray(),
'counts' => $reasonCounts->pluck('count')->toArray()
];
return response()->json([
'status_data' => $statusData,
'reason_data' => $reasonData
]);
}
}