1, 'Reachable' => 2, 'Unreachable' => 3, 'LOGGED_OUT' => 4, 'unpause' => 5, 'pause' => 6, ]; public function __construct() { $this->SettingsHelper = new SettingsHelper(); $this->UserHelper = new UserHelper(); } private function respond($success, $data = [], $errors = [], $message = '', $code = 200) { return response()->json([ 'success' => $success, 'data' => $data, 'errors' => $errors, 'message' => $message, ], $code); } public function attendance(Request $request) { try { $user = Auth::user(); $conds = $this->SettingsHelper->defaultConds(); if ($request->has('daterange')) { $range = explode(' - ', $request->daterange); try { $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'); } } $company_users = $this->UserHelper->getCompanyUsers(); if ($user->access_to_all_entity != '1') { $company_users = User::whereHas('groups', function ($q) { $q->whereIn('groups.id', auth()->user()->accessibleQueueIds()); }) ->whereDoesntHave('roles', fn($q) => $q->where('name', 'company_manager')) ->get(); } $company_user_ids = $company_users->pluck('id')->toArray(); // Data source: attendance (canonical AMI event store) — same table // and logic as the web AttendanceController. Attendance events are // no longer written to activity_log. $db = config('database.connections.mysql.database'); $query = DB::table("{$db}.attendance") ->select( 'attendance.created_at', DB::raw('attendance.subject_id as agent_id'), DB::raw('attendance.description as status'), DB::raw('attendance.log_name as pause_reason'), 'users.name as agent_name', 'users.email as agent_email' ) ->join("{$db}.users", "{$db}.users.id", '=', "{$db}.attendance.subject_id") ->whereIn('attendance.subject_id', $company_user_ids) ->whereBetween('attendance.created_at', [$conds['from'], $conds['to']]); // Queue restriction is already enforced above via $company_user_ids // (same accessibleQueueIds() scope as the web report). if ($request->filled('user_ext')) { $query->where('attendance.subject_id', $request->user_ext); } elseif ($request->filled('group_ext')) { $groupUserIds = User::join('group_user', "{$db}.users.id", '=', 'group_user.user_id') ->where('group_user.group_id', $request->group_ext) ->where('users.company_id', $user->company->id ?? 1) ->pluck('users.id')->toArray(); if (!empty($groupUserIds)) { $query->whereIn('attendance.subject_id', $groupUserIds); } } if ($user->roles[0]->name === 'user' && $user->access_to_all_entity != '1') { $query->where('attendance.subject_id', $user->id); } $query->orderBy('attendance.subject_id')->orderBy('attendance.created_at', 'ASC'); $perPage = (int) $request->get('per_page', 10); $result = $query->paginate($perPage); $items = $result->getCollection()->values(); $result->setCollection($items->map(function ($item, $idx) use ($items) { $next = $items->slice($idx + 1)->first(fn($n) => $n->agent_id === $item->agent_id); if ($next) { $diff = Carbon::parse($item->created_at)->diff(Carbon::parse($next->created_at)); $item->duration = sprintf('%02dd %02dh %02dm %02ds', $diff->d, $diff->h, $diff->i, $diff->s); } else { $item->duration = 'Ongoing'; } $item->event_time = Carbon::parse($item->created_at)->format('Y-m-d H:i:s'); unset($item->created_at); return $item; })); return $this->respond(true, [ 'data' => $result, 'filters' => [ 'daterange' => $conds['from'] . ' - ' . $conds['to'], 'user_ext' => $request->user_ext ?? null, 'group_ext' => $request->group_ext ?? null, ], ], [], 'Attendance data retrieved successfully'); } catch (\Throwable $th) { \Log::error('Attendance API: ' . $th->getMessage()); return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } public function summary(Request $request) { try { $user = Auth::user(); $conds = $this->SettingsHelper->defaultConds(); if ($request->has('daterange')) { $range = explode(' - ', $request->daterange); try { $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'); } } $summaryData = $this->calculateAttendanceSummary( $conds, $request->filled('user_ext') ? $request->user_ext : null, $request->filled('group_ext') ? $request->group_ext : null ); // Dropdown options — same permission scope as the web Attendance // report's filter dropdowns (shape unchanged: agents/groups maps). $agent_names = $this->UserHelper->getCompanyUsers(); if ($user->access_to_all_entity != '1') { $agent_names = User::whereHas('groups', function ($q) { $q->whereIn('groups.id', auth()->user()->accessibleQueueIds()); }) ->whereDoesntHave('roles', fn($q) => $q->where('name', 'company_manager')) ->visibleInReports() ->get(); } $groups = Group::where('company_id', $user->company->id ?? 1)->pluck('name', 'id'); if ($user->access_to_all_entity != '1') { $groups = Group::whereIn('id', $user->accessibleQueueIds())->pluck('name', 'id'); if ($user->roles[0]->name == 'user') { $groups = collect(); } } if ($user->roles[0]->name == 'user' && $user->access_to_all_entity != '1') { $agent_names = $agent_names->where('id', $user->id); } return $this->respond(true, [ 'summary' => array_values($summaryData), 'filters' => [ 'daterange' => $conds['from'] . ' - ' . $conds['to'], 'user_ext' => $request->user_ext ?? null, 'group_ext' => $request->group_ext ?? null, ], 'options' => [ 'agents' => $agent_names->pluck('name', 'id'), 'groups' => $groups, ], ], [], 'Attendance summary retrieved successfully'); } catch (\Throwable $th) { \Log::error('Attendance Summary API: ' . $th->getMessage()); return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } public function users(Request $request) { try { $user = Auth::user(); // Same option scope as the web Attendance report's agent dropdown. $company_users = $this->UserHelper->getCompanyUsers(); if ($user->access_to_all_entity != '1') { $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(); } if ($user->roles[0]->name == 'user' && $user->access_to_all_entity != '1') { $company_users = $company_users->where('id', $user->id); } $company_users = $company_users->map(function ($user) { return $user->only(['id', 'name', 'email']); })->values(); return $this->respond(true, ['agents' => $company_users], [], 'Users retrieved'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } public function groups(Request $request) { try { $user = Auth::user(); $groups = Group::where('company_id',$user->company->id ?? 1)->get(['id', 'name']); if($user->access_to_all_entity != '1'){ $groups = Group::whereIn('id', $user->accessibleQueueIds())->get(['id', 'name']); if($user->roles[0]->name == 'user'){ $groups = null; } } return $this->respond(true, ['groups' => $groups], [], 'Groups retrieved'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } // ───────────────────────────────────────────────────────────────────────── // Private helpers – ported from web AttendanceController // ───────────────────────────────────────────────────────────────────────── protected function calculateAttendanceSummary($conds, $user_ext, $group_ext) { $user = Auth::user(); $company_users = $this->UserHelper->getCompanyUsers(); if ($user->access_to_all_entity != '1') { $company_users = User::whereHas('groups', function ($q) { $q->whereIn('groups.id', auth()->user()->accessibleQueueIds()); }) ->whereDoesntHave('roles', fn($q) => $q->where('name', 'company_manager')) ->get(); } $company_user_ids = $company_users->pluck('id')->toArray(); $query = DB::table('attendance') ->select( 'attendance.id', 'attendance.created_at', 'attendance.subject_id', 'attendance.description', 'attendance.log_name', 'users.name', 'users.email', 'users.code' ) ->join('users', 'users.id', '=', 'attendance.subject_id') ->whereIn('attendance.subject_id', $company_user_ids) ->whereBetween('attendance.created_at', [$conds['from'], $conds['to']]) ->where('description_code', '!=', self::DESCRIPTION_CODES['LOGGED_OUT']); if (isset($user_ext)) { $query->where('subject_id', $user_ext); } elseif (isset($group_ext)) { $groupUserIds = DB::table('group_user')->where('group_id', $group_ext)->pluck('user_id')->toArray(); $query->whereIn('subject_id', $groupUserIds); } if ($user->roles[0]->name === 'user' && $user->access_to_all_entity != '1') { $query->where('subject_id', $user->id); } $data = $query->orderBy('attendance.subject_id')->orderBy('attendance.id', 'ASC')->get(); // Pre-fetch last action before range start for each user $previousActions = []; foreach ($data->pluck('subject_id')->unique() as $userId) { $previousActions[$userId] = DB::table('attendance') ->where('subject_id', $userId) ->where('created_at', '<', $conds['from']) ->whereNotIn('description_code', [self::DESCRIPTION_CODES['LOGGED_OUT'], self::DESCRIPTION_CODES['LOGGED_IN']]) ->orderBy('id', 'DESC') ->first(); } $summary = []; $effectiveEndTime = Carbon::now()->min(Carbon::parse($conds['to'])); foreach ($data as $key => $item) { $userId = $item->subject_id; if (!isset($summary[$userId])) { $summary[$userId] = [ 'name' => $item->name, 'email' => $item->email, 'code' => $item->code ?? '', 'LOGGED_IN' => 0, 'pause' => 0, 'Reachable' => 0, 'total' => 0, 'pause_reasons' => [], ]; $firstActionTime = Carbon::parse($item->created_at); $periodStart = Carbon::parse($conds['from']); if ($firstActionTime->gt($periodStart)) { $initialDuration = $periodStart->diffInSeconds($firstActionTime); $prev = $previousActions[$userId] ?? null; if ($prev) { if (in_array($prev->description, ['Reachable', 'unpause'])) { $summary[$userId]['Reachable'] += $initialDuration; } elseif ($prev->description === 'pause') { $summary[$userId]['pause'] += $initialDuration; if ($prev->log_name) { $this->addPauseReason($summary[$userId]['pause_reasons'], $prev->log_name, $initialDuration); } } $summary[$userId]['total'] += $initialDuration; } } } if ($item->description === 'pause' && $item->log_name) { $this->addPauseReason($summary[$userId]['pause_reasons'], $item->log_name, 0); } $nextItem = $data->get($key + 1); $isLast = !$nextItem || $nextItem->subject_id !== $userId; $duration = $isLast ? Carbon::parse($item->created_at)->diffInSeconds($effectiveEndTime) : Carbon::parse($item->created_at)->diffInSeconds(Carbon::parse($nextItem->created_at)); if ($item->description === 'LOGGED_IN') { $prevEvent = $this->findPreviousEventForUser($data, $key, $userId); if ($prevEvent && in_array($prevEvent->description, ['Reachable', 'unpause'])) { $summary[$userId]['Reachable'] += $duration; } elseif ($prevEvent && $prevEvent->description === 'pause') { $summary[$userId]['pause'] += $duration; if ($prevEvent->log_name) { $this->addPauseReason($summary[$userId]['pause_reasons'], $prevEvent->log_name, $duration); } } else { $summary[$userId]['LOGGED_IN'] += $duration; } } else { $this->addDurationToSummary($summary[$userId], $item->description, $duration, $item->log_name); } $summary[$userId]['total'] += $duration; } foreach ($summary as &$s) { // API contract (unchanged) — *_time keys keep their existing names. $s['logged_in_time'] = $this->secondsToTime($s['LOGGED_IN']); $s['pause_time'] = $this->secondsToTime($s['pause']); $s['reachable_time'] = $this->secondsToTime($s['Reachable']); $s['total_time'] = $this->secondsToTime($s['total']); // Additive: the *_formatted keys exposed by the web report, so both // implementations surface the same fields. Existing keys above are // untouched. $s['LOGGED_IN_formatted'] = $this->secondsToTime($s['LOGGED_IN']); $s['pause_formatted'] = $this->secondsToTime($s['pause']); $s['Reachable_formatted'] = $this->secondsToTime($s['Reachable']); $s['total_formatted'] = $this->secondsToTime($s['total']); $s['pause_reasons'] = array_values(array_filter($s['pause_reasons'])); unset($s['LOGGED_IN'], $s['pause'], $s['Reachable'], $s['total']); } return $summary; } protected function findPreviousEventForUser($data, $currentIndex, $userId) { for ($i = $currentIndex - 1; $i >= 0; $i--) { $item = $data->get($i); if ($item && $item->subject_id === $userId && !in_array($item->description, ['LOGGED_IN', 'LOGGED_OUT'])) { return $item; } } return null; } protected function addDurationToSummary(&$summary, $description, $duration, $reason = null) { if (in_array($description, ['Reachable', 'unpause'])) { $summary['Reachable'] += $duration; } elseif ($description === 'pause') { $summary['pause'] += $duration; if ($reason) { $this->addPauseReason($summary['pause_reasons'], $reason, $duration); } } elseif (in_array($description, ['LOGGED_IN', 'Unreachable'])) { $summary['LOGGED_IN'] += $duration; } } protected function addPauseReason(&$reasonsArray, $reason, $duration) { if (!isset($reasonsArray[$reason])) { $reasonsArray[$reason] = ['reason' => $reason, 'duration' => 0, 'formatted_duration' => '00:00:00']; } $reasonsArray[$reason]['duration'] += $duration; $reasonsArray[$reason]['formatted_duration'] = $this->secondsToTime($reasonsArray[$reason]['duration']); } protected function secondsToTime($seconds) { return sprintf('%02d:%02d:%02d', floor($seconds / 3600), floor(($seconds % 3600) / 60), $seconds % 60); } /** * GET /api/reporting/attendance/pause-reasons * * Returns all available pause reasons from the user_status table, * plus which reasons have actually been used in the activity log. * Mirrors AttendanceController::getAvailablePauseReasons(). */ public function getAvailablePauseReasons() { try { $pauseReasons = DB::table('user_status') ->select('name', 'command', 'color', 'icon') ->where('command', 'pause') ->get(); $usedReasons = DB::table('attendance') ->where('description_code', self::DESCRIPTION_CODES['pause']) ->whereNotNull('log_name') ->select('log_name as reason') ->distinct() ->get(); return $this->respond(true, [ 'available_reasons' => $pauseReasons, 'used_reasons' => $usedReasons, ], [], 'Pause reasons retrieved'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } /** * GET /api/reporting/attendance/pause-stats * * Returns statistics per pause reason (occurrence count + total duration). * Mirrors AttendanceController::getPauseReasonsStatistics(). * * Filters (query params): * - daterange : "YYYY-MM-DD HH:mm:ss - YYYY-MM-DD HH:mm:ss". Defaults to today. */ public function getPauseReasonsStatistics(Request $request) { try { $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'); } } $nowStr = Carbon::now()->format('Y-m-d H:i:s'); $statistics = DB::table('attendance') ->join('users', 'users.id', '=', 'attendance.subject_id') ->whereBetween('attendance.created_at', [$conds['from'], $conds['to']]) ->where('description_code', self::DESCRIPTION_CODES['pause']) ->whereNotNull('log_name') ->select( 'log_name as reason', DB::raw('COUNT(*) as occurrence_count'), DB::raw("SUM(TIMESTAMPDIFF(SECOND, attendance.created_at, COALESCE( (SELECT next.created_at FROM attendance next WHERE next.subject_id = attendance.subject_id AND next.id > attendance.id AND next.description_code IN (" . self::DESCRIPTION_CODES['unpause'] . ", " . self::DESCRIPTION_CODES['Reachable'] . ") ORDER BY next.id ASC LIMIT 1), '" . $nowStr . "' ) )) as total_duration") ) ->groupBy('log_name') ->get(); foreach ($statistics as $stat) { $stat->formatted_duration = $this->secondsToTime((int) $stat->total_duration); } return $this->respond(true, [ 'statistics' => $statistics, 'filters' => [ 'daterange' => $conds['from'] . ' - ' . $conds['to'], ], ], [], 'Pause reason statistics retrieved'); } catch (\Throwable $th) { return $this->respond(false, [], ['error' => $th->getMessage()], 'Internal Server Error', 500); } } }