diff --git a/app/Commands/SendActivityDigest.php b/app/Commands/SendActivityDigest.php
new file mode 100644
index 0000000..656ac34
--- /dev/null
+++ b/app/Commands/SendActivityDigest.php
@@ -0,0 +1,209 @@
+ 'Digest period: daily, weekly, or monthly (default: daily)',
+ ];
+
+ public function run(array $params = [])
+ {
+ $period = $params[0] ?? 'daily';
+
+ if (!in_array($period, ['daily', 'weekly', 'monthly'])) {
+ CLI::error('Invalid period. Use: daily, weekly, or monthly');
+ return;
+ }
+
+ $activityModel = new ActivityLogModel();
+ $userModel = new UserModel();
+
+ // Determine date range
+ $startDate = match($period) {
+ 'daily' => date('Y-m-d H:i:s', strtotime('-1 day')),
+ 'weekly' => date('Y-m-d H:i:s', strtotime('-7 days')),
+ 'monthly' => date('Y-m-d H:i:s', strtotime('-30 days')),
+ default => date('Y-m-d H:i:s', strtotime('-1 day')),
+ };
+
+ // Get activity summary for the period
+ $db = \Config\Database::connect();
+ $logs = $db->table('activity_logs')
+ ->where('activity_at >=', $startDate)
+ ->orderBy('activity_at', 'DESC')
+ ->get()
+ ->getResultArray();
+
+ if (empty($logs)) {
+ CLI::write('No activity found for ' . $period . ' digest', 'yellow');
+ return;
+ }
+
+ // Get admin users
+ $admins = $userModel->where('role', 'admin')->findAll();
+
+ if (empty($admins)) {
+ CLI::error('No admin users found to send digest to');
+ return;
+ }
+
+ // Send email to each admin
+ $email = service('email');
+ $emailConfig = config('Email');
+
+ $successCount = 0;
+ $failCount = 0;
+
+ foreach ($admins as $admin) {
+ $html = $this->generateDigestHTML($logs, $period, $admin);
+
+ $email->setFrom($emailConfig->fromEmail, $emailConfig->fromName)
+ ->setTo($admin['email'])
+ ->setSubject(ucfirst($period) . ' Activity Digest - ' . date('Y-m-d'))
+ ->setMessage($html);
+
+ if ($email->send(false)) {
+ $successCount++;
+ CLI::write('Email sent to: ' . $admin['email'], 'green');
+ } else {
+ $failCount++;
+ CLI::error('Failed to send email to: ' . $admin['email']);
+ }
+
+ $email->clear();
+ }
+
+ CLI::write("\nDigest email summary:", 'cyan');
+ CLI::write('Sent: ' . $successCount, 'green');
+ CLI::write('Failed: ' . $failCount, 'red');
+ }
+
+ protected function generateDigestHTML($logs, $period, $admin)
+ {
+ $totalActions = count($logs);
+
+ // Group by action
+ $byAction = [];
+ foreach ($logs as $log) {
+ $action = $log['action'];
+ $byAction[$action] = ($byAction[$action] ?? 0) + 1;
+ }
+
+ // Get critical actions
+ $criticalActions = array_filter($logs, function($log) {
+ return stripos($log['action'], 'delete') !== false;
+ });
+
+ $actionTypeCount = count($byAction);
+ $criticalActionCount = count($criticalActions);
+
+ $html = <<
+
+
+
+
+
+
+
+
+
+
+
+
+
{$totalActions}
+
Total Actions
+
+
+
{$actionTypeCount}
+
Action Types
+
+
+
{$criticalActionCount}
+
Critical Actions
+
+
+
+
Top Actions
+
+
+
+ Action
+ Count
+
+
+
+HTML;
+
+ arsort($byAction);
+ foreach (array_slice($byAction, 0, 10) as $action => $count) {
+ $isCritical = stripos($action, 'delete') !== false ? 'class="critical"' : '';
+ $html .= "{$action} {$count} ";
+ }
+
+ $html .= <<
+
+
+
Critical Actions (Deletions)
+HTML;
+
+ if (!empty($criticalActions)) {
+ $html .= '
Time User Action Target ';
+ foreach (array_slice($criticalActions, 0, 20) as $log) {
+ $userId = $log['activity_user_id'] ?? 'System';
+ $targetType = $log['target_user_type'] ?? '-';
+ $html .= "";
+ $html .= "" . date('Y-m-d H:i', strtotime($log['activity_at'])) . " ";
+ $html .= "{$userId} ";
+ $html .= "{$log['action']} ";
+ $html .= "{$targetType} ";
+ $html .= " ";
+ }
+ $html .= '
';
+ } else {
+ $html .= '
No critical actions detected.
';
+ }
+
+ $html .= <<
+
This is an automated email. Please do not reply to this message.
+
Generated on {date('Y-m-d H:i:s')}
+
+
+
+
+
+HTML;
+
+ return $html;
+ }
+}
diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php
index 4e6e184..1f3a741 100644
--- a/app/Config/Autoload.php
+++ b/app/Config/Autoload.php
@@ -88,5 +88,5 @@ class Autoload extends AutoloadConfig
*
* @var list
*/
- public $helpers = ['form', 'url', 'encryption'];
+ public $helpers = ['form', 'url', 'encryption', 'activity'];
}
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index de6aa42..d2f5bdc 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -47,3 +47,7 @@ $routes->post('/check-email', 'Admin::checkEmail');
$routes->get('admin/doctors/data', 'Admin::getDoctors');
$routes->get('admin/patients/data', 'Admin::getPatients');
$routes->get('/admin/activity-log', 'ActivityLog::index');
+$routes->get('/admin/activity/analytics', 'ActivityLog::analytics');
+// $routes->post('/admin/activity-log/datatable', 'ActivityLog::datatable');
+// $routes->get('/admin/activity-log/summary', 'ActivityLog::getSummary');
+// $routes->get('/admin/activity-log/critical', 'ActivityLog::getCritical');
diff --git a/app/Controllers/ActivityLog.php b/app/Controllers/ActivityLog.php
index 1f104c9..482bcf2 100644
--- a/app/Controllers/ActivityLog.php
+++ b/app/Controllers/ActivityLog.php
@@ -12,17 +12,87 @@ class ActivityLog extends BaseController
return $r;
}
- $logModel = new ActivityLogModel();
- $filters = [
- 'action' => trim((string) $this->request->getGet('action')),
- 'role' => trim((string) $this->request->getGet('role')),
- 'date_from' => trim((string) $this->request->getGet('date_from')),
- 'date_to' => trim((string) $this->request->getGet('date_to')),
- ];
+ $activityModel = new ActivityLogModel();
+
+ // Get filter parameters
+ $search = $this->request->getGet('search') ?? '';
+ $action = $this->request->getGet('action') ?? '';
+ $userType = $this->request->getGet('user_type') ?? '';
+ $dateFrom = $this->request->getGet('date_from') ?? '';
+ $dateTo = $this->request->getGet('date_to') ?? '';
+ $ip = $this->request->getGet('ip') ?? '';
+
+ // Build filters array
+ $filters = [];
+ if (!empty($search)) $filters['search'] = $search;
+ if (!empty($action)) $filters['action'] = $action;
+ if (!empty($userType)) $filters['user_type'] = $userType;
+ if (!empty($dateFrom)) $filters['date_from'] = $dateFrom;
+ if (!empty($dateTo)) $filters['date_to'] = $dateTo;
+ if (!empty($ip)) $filters['ip'] = $ip;
+
+ // Get filtered logs
+ $logs = $activityModel->getFilteredLogs($filters, 200);
return view('admin/activity_log', [
- 'logs' => $logModel->getFiltered($filters),
- 'filters' => $filters,
+ 'logs' => $logs,
+ 'totalLogs' => count($logs),
+ 'actionSummary' => $activityModel->getActionSummary(),
+ 'roleSummary' => $activityModel->getRoleSummary(),
+ 'filters' => [
+ 'search' => $search,
+ 'action' => $action,
+ 'user_type' => $userType,
+ 'date_from' => $dateFrom,
+ 'date_to' => $dateTo,
+ 'ip' => $ip,
+ ],
+ 'availableActions' => $activityModel->getAvailableActions(),
+ 'actionIcons' => [
+ 'login' => 'box-arrow-in-right',
+ 'logout' => 'box-arrow-right',
+ 'register_patient' => 'person-plus',
+ 'register_doctor' => 'person-badge-plus',
+ 'add_doctor' => 'person-badge-plus',
+ 'update_doctor' => 'person-badge',
+ 'delete_doctor' => 'person-badge-x',
+ 'add_patient' => 'person-plus',
+ 'update_patient' => 'person',
+ 'delete_patient' => 'person-x',
+ 'book_appointment' => 'calendar-plus',
+ 'update_appointment' => 'calendar-check',
+ 'cancel_appointment' => 'calendar-x',
+ 'view_appointment' => 'calendar-event',
+ ],
]);
}
+
+ public function analytics()
+ {
+ if ($r = $this->requireRole('admin')) {
+ return $r;
+ }
+
+ $period = $this->request->getGet('period') ?? '7_days';
+ if (! in_array($period, ['7_days', '30_days'], true)) {
+ $period = '7_days';
+ }
+
+ $activityModel = new ActivityLogModel();
+ $summary = $activityModel->getSummary($period);
+
+ return view('admin/activity_analytics', [
+ 'period' => $period,
+ 'summary' => $summary,
+ 'actionLabels' => json_encode(array_keys($summary['by_action'])),
+ 'actionCounts' => json_encode(array_values($summary['by_action'])),
+ 'typeLabels' => json_encode(array_keys($summary['by_role'])),
+ 'typeCounts' => json_encode(array_values($summary['by_role'])),
+ 'userLabels' => json_encode(array_column($summary['most_active_users'], 'actor')),
+ 'userCounts' => json_encode(array_column($summary['most_active_users'], 'count')),
+ 'uniqueIPs' => $summary['unique_ips'],
+ 'criticalActions' => $activityModel->getCriticalActions(50),
+ ]);
+ }
+
}
diff --git a/app/Database/Migrations/2026-04-15-120000_AlterActivityLogsTable.php b/app/Database/Migrations/2026-04-15-120000_AlterActivityLogsTable.php
new file mode 100644
index 0000000..fc7a9d6
--- /dev/null
+++ b/app/Database/Migrations/2026-04-15-120000_AlterActivityLogsTable.php
@@ -0,0 +1,132 @@
+db->disableForeignKeyChecks();
+ $this->forge->dropTable('activity_logs', true);
+ $this->db->enableForeignKeyChecks();
+
+ // Create the new table with the specified structure
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'BIGINT',
+ 'auto_increment' => true,
+ ],
+ 'ip' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 45,
+ 'null' => true,
+ ],
+ 'action' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 100,
+ 'null' => false,
+ ],
+ 'description' => [
+ 'type' => 'TEXT',
+ 'null' => true,
+ ],
+ 'activity_user_id' => [
+ 'type' => 'BIGINT',
+ 'null' => true,
+ ],
+ 'activity_user_type' => [
+ 'type' => 'ENUM',
+ 'constraint' => ['admin', 'doctor', 'patient'],
+ 'null' => true,
+ ],
+ 'target_user_id' => [
+ 'type' => 'BIGINT',
+ 'null' => true,
+ ],
+ 'target_user_type' => [
+ 'type' => 'ENUM',
+ 'constraint' => ['admin', 'doctor', 'patient'],
+ 'null' => true,
+ ],
+ 'activity_page' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
+ 'activity_at' => [
+ 'type' => 'DATETIME',
+ 'null' => false,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->addKey('activity_user_id');
+ $this->forge->addKey('target_user_id');
+ $this->forge->addKey('action');
+ $this->forge->addKey('activity_at');
+ $this->forge->addKey('ip');
+
+ $this->forge->createTable('activity_logs');
+ }
+
+ public function down()
+ {
+ // Revert to original table structure if rollback
+ $this->db->disableForeignKeyChecks();
+ $this->forge->dropTable('activity_logs', true);
+ $this->db->enableForeignKeyChecks();
+
+ // Recreate original structure
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'BIGINT',
+ 'auto_increment' => true,
+ ],
+ 'actor_user_id' => [
+ 'type' => 'BIGINT',
+ 'null' => true,
+ ],
+ 'actor_role' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 50,
+ 'null' => true,
+ ],
+ 'action' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 100,
+ ],
+ 'description' => [
+ 'type' => 'TEXT',
+ 'null' => true,
+ ],
+ 'target_type' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 100,
+ 'null' => true,
+ ],
+ 'target_id' => [
+ 'type' => 'BIGINT',
+ 'null' => true,
+ ],
+ 'ip_address' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 45,
+ 'null' => true,
+ ],
+ 'user_agent' => [
+ 'type' => 'TEXT',
+ 'null' => true,
+ ],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => false,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->createTable('activity_logs');
+ }
+}
diff --git a/app/Helpers/activity_helper.php b/app/Helpers/activity_helper.php
new file mode 100644
index 0000000..18d7d5c
--- /dev/null
+++ b/app/Helpers/activity_helper.php
@@ -0,0 +1,100 @@
+get('id');
+ return $userId ? (int) $userId : null;
+}
+
+/**
+ * Get the current activity user type/role from session
+ *
+ * @return string The user role (admin, doctor, patient) or 'guest'
+ */
+function getActivityUserType(): string
+{
+ return session()->get('role') ?: 'guest';
+}
+
+/**
+ * Get the current page/path being accessed
+ *
+ * @return string The current request path
+ */
+function getActivityPage(): string
+{
+ $request = service('request');
+ return $request->getPath();
+}
+
+/**
+ * Get the client IP address
+ *
+ * @return string|null The client IP address or null if unavailable
+ */
+function getActivityIP(): ?string
+{
+ $request = service('request');
+ if (method_exists($request, 'getIPAddress')) {
+ return $request->getIPAddress();
+ }
+ return null;
+}
+
+/**
+ * Get all activity metadata at once
+ *
+ * Useful for logging operations that need complete activity context
+ *
+ * @return array Array containing user_id, user_type, page, and ip
+ */
+function getActivityMetadata(): array
+{
+ return [
+ 'user_id' => getActivityUserId(),
+ 'user_type' => getActivityUserType(),
+ 'page' => getActivityPage(),
+ 'ip' => getActivityIP(),
+ ];
+}
+
+/**
+ * Check if current user is authenticated
+ *
+ * @return bool True if user is logged in, false otherwise
+ */
+function isActivityUserAuthenticated(): bool
+{
+ return getActivityUserId() !== null;
+}
+
+/**
+ * Get formatted user identifier for logging
+ *
+ * Returns a string like "User #123 (admin)" or "Guest"
+ *
+ * @return string Formatted user identifier
+ */
+function getFormattedActivityUser(): string
+{
+ $userId = getActivityUserId();
+ $userType = getActivityUserType();
+
+ if ($userId === null) {
+ return 'Guest';
+ }
+
+ return "User #{$userId} ({$userType})";
+}
diff --git a/app/Helpers/time_helper.php b/app/Helpers/time_helper.php
new file mode 100644
index 0000000..ef01a03
--- /dev/null
+++ b/app/Helpers/time_helper.php
@@ -0,0 +1,3 @@
+get('id');
+ $actorRole = $actorRole ?? session()->get('role') ?? 'guest';
- $this->insert([
- 'actor_user_id' => session()->get('id') ? (int) session()->get('id') : null,
- 'actor_role' => session()->get('role') ?: null,
- 'action' => $action,
- 'description' => $description,
- 'target_type' => $targetType,
- 'target_id' => $targetId,
- 'ip_address' => method_exists($request, 'getIPAddress') ? $request->getIPAddress() : null,
- 'user_agent' => method_exists($request, 'getUserAgent') ? $request->getUserAgent()->getAgentString() : null,
- 'created_at' => date('Y-m-d H:i:s'),
+ $request = service('request');
+ $ipAddress = $ipAddress ?? ($request->hasHeader('X-Forwarded-For') ? trim(explode(',', $request->getHeaderLine('X-Forwarded-For'))[0]) : $request->getIPAddress());
+ $userAgent = $userAgent ?? ($request->hasHeader('User-Agent') ? $request->getHeaderLine('User-Agent') : null);
+ $validTargetTypes = ['admin', 'doctor', 'patient'];
+ $targetUserType = in_array($targetType, $validTargetTypes, true) ? $targetType : null;
+
+ return (bool) $this->insert([
+ 'ip' => $ipAddress,
+ 'action' => $action,
+ 'description' => $description,
+ 'activity_user_id' => $actorId,
+ 'activity_user_type' => $actorRole,
+ 'target_user_id' => $targetId,
+ 'target_user_type' => $targetUserType,
+ 'activity_page' => $request->getPath(),
+ 'activity_at' => date('Y-m-d H:i:s'),
]);
}
- public function getFiltered(array $filters = []): array
+ public function getFilteredLogs(array $filters = [], int $limit = 200): array
{
- $builder = $this->db->table('activity_logs al')
- ->select("al.*, CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, '')) AS actor_name, u.email AS actor_email")
- ->join('users u', 'u.id = al.actor_user_id', 'left');
+ $builder = $this->select('activity_logs.*, COALESCE(NULLIF(CONCAT(users.first_name, " ", users.last_name), " "), users.email, "Guest") AS actor_name')
+ ->join('users', 'users.id = activity_logs.activity_user_id', 'left');
- if (! empty($filters['action'])) {
- $builder->like('al.action', $filters['action']);
+ // Apply filters
+ if (!empty($filters['search'])) {
+ $search = $filters['search'];
+ $builder->groupStart()
+ ->like('users.first_name', $search)
+ ->orLike('users.last_name', $search)
+ ->orLike('users.email', $search)
+ ->orLike('activity_logs.description', $search)
+ ->groupEnd();
}
- if (! empty($filters['role'])) {
- $builder->where('al.actor_role', $filters['role']);
+ if (!empty($filters['action'])) {
+ $builder->where('activity_logs.action', $filters['action']);
}
- if (! empty($filters['date_from'])) {
- $builder->where('DATE(al.created_at) >=', $filters['date_from']);
+ if (!empty($filters['user_type'])) {
+ $builder->where('activity_logs.activity_user_type', $filters['user_type']);
}
- if (! empty($filters['date_to'])) {
- $builder->where('DATE(al.created_at) <=', $filters['date_to']);
+ if (!empty($filters['date_from'])) {
+ $builder->where('activity_logs.activity_at >=', $filters['date_from'] . ' 00:00:00');
}
- return $builder
- ->orderBy('al.created_at', 'DESC')
- ->get()
- ->getResultArray();
+ if (!empty($filters['date_to'])) {
+ $builder->where('activity_logs.activity_at <=', $filters['date_to'] . ' 23:59:59');
+ }
+
+ if (!empty($filters['ip'])) {
+ $builder->where('activity_logs.ip', $filters['ip']);
+ }
+
+ return $builder->orderBy('activity_logs.activity_at', 'DESC')
+ ->findAll($limit);
}
- public function getRecent(int $limit = 8): array
+ public function getAvailableActions(): array
{
- return $this->db->table('activity_logs')
- ->orderBy('created_at', 'DESC')
- ->limit($limit)
- ->get()
- ->getResultArray();
+ $rows = $this->select('action')
+ ->distinct()
+ ->orderBy('action', 'ASC')
+ ->findAll();
+
+ return array_column($rows, 'action');
+ }
+
+ public function getActionSummary(): array
+ {
+ $rows = $this->select('action, COUNT(*) AS count')
+ ->groupBy('action')
+ ->orderBy('count', 'DESC')
+ ->findAll();
+
+ return array_column($rows, 'count', 'action');
+ }
+
+ public function getRoleSummary(): array
+ {
+ $rows = $this->select('activity_user_type AS actor_role, COUNT(*) AS count')
+ ->groupBy('activity_user_type')
+ ->orderBy('count', 'DESC')
+ ->findAll();
+
+ return array_column($rows, 'count', 'actor_role');
+ }
+
+ public function getSummary(string $period = '7_days'): array
+ {
+ $period = $period === '30_days' ? '30_days' : '7_days';
+ $startDate = $period === '30_days' ? date('Y-m-d H:i:s', strtotime('-30 days')) : date('Y-m-d H:i:s', strtotime('-7 days'));
+
+ return [
+ 'total_actions' => (int) $this->where('activity_at >=', $startDate)->countAllResults(false),
+ 'by_action' => $this->getActionSummaryForPeriod($startDate),
+ 'by_role' => $this->getRoleSummaryForPeriod($startDate),
+ 'most_active_users' => $this->getMostActiveUsers($startDate),
+ 'unique_ips' => $this->getUniqueIPs($startDate),
+ ];
+ }
+
+ protected function getActionSummaryForPeriod(string $startDate): array
+ {
+ $rows = $this->select('action, COUNT(*) AS count')
+ ->where('activity_at >=', $startDate)
+ ->groupBy('action')
+ ->orderBy('count', 'DESC')
+ ->findAll();
+
+ return array_column($rows, 'count', 'action');
+ }
+
+ protected function getRoleSummaryForPeriod(string $startDate): array
+ {
+ $rows = $this->select('activity_user_type AS actor_role, COUNT(*) AS count')
+ ->where('activity_at >=', $startDate)
+ ->groupBy('activity_user_type')
+ ->orderBy('count', 'DESC')
+ ->findAll();
+
+ return array_column($rows, 'count', 'actor_role');
+ }
+
+ protected function getMostActiveUsers(string $startDate, int $limit = 10): array
+ {
+ return $this->select('COALESCE(NULLIF(TRIM(CONCAT(users.first_name, " ", users.last_name)), ""), users.email, "Guest") AS actor, COUNT(activity_logs.id) AS count')
+ ->join('users', 'users.id = activity_logs.activity_user_id', 'left')
+ ->where('activity_logs.activity_at >=', $startDate)
+ ->groupBy('activity_logs.activity_user_id')
+ ->orderBy('count', 'DESC')
+ ->findAll($limit);
+ }
+
+ protected function getUniqueIPs(string $startDate, int $limit = 10): array
+ {
+ return $this->select('ip, COUNT(*) AS count')
+ ->where('activity_at >=', $startDate)
+ ->groupBy('ip')
+ ->orderBy('count', 'DESC')
+ ->findAll($limit);
+ }
+
+ public function getCriticalActions(int $limit = 50): array
+ {
+ $criticalActions = ['admin_login', 'admin_password_change', 'system_error', 'security_breach'];
+
+ return $this->select('activity_logs.*, COALESCE(NULLIF(TRIM(CONCAT(users.first_name, " ", users.last_name)), ""), users.email, "System") AS actor_name, users.email AS actor_email')
+ ->join('users', 'users.id = activity_logs.activity_user_id', 'left')
+ ->whereIn('action', $criticalActions)
+ ->orderBy('activity_at', 'DESC')
+ ->findAll($limit);
}
}
diff --git a/app/Views/admin/activity_analytics.php b/app/Views/admin/activity_analytics.php
new file mode 100644
index 0000000..1979d6d
--- /dev/null
+++ b/app/Views/admin/activity_analytics.php
@@ -0,0 +1,322 @@
+ 20 ? substr($label, 0, 20) . '...' : $label;
+}
+?>
+
+
+
+
+
+ Activity Analytics Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= $summary['total_actions'] ?? 0 ?>
+
Total Actions
+
+
+
+
+
= count($summary['by_action'] ?? []) ?>
+
Action Types
+
+
+
+
+
= count($summary['by_role'] ?? []) ?>
+
Active Roles
+
+
+
+
+
= count($summary['most_active_users'] ?? []) ?>
+
Active Users
+
+
+
+
+
+
+
+
+
No Activity Data Available
+
There are no activity logs recorded yet. Once you start using the system (login, create/update records, etc.), the analytics dashboard will display detailed statistics and charts.
+
+
+
+
+
+
Actions Distribution
+
+
+
+
+
+
Activity by Role
+
+
+
+
+
+
+
+
+
Most Active Users
+
+
+
+
+
+
Top IP Addresses
+
+
+
+
+ IP Address
+ Count
+
+
+
+
+
+
+ = esc($ip['ip']) ?>
+ = $ip['count'] ?>
+
+
+
+
+ No IP data available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Timestamp
+ User
+ Action
+ Target
+ Details
+
+
+
+
+
+ No critical actions found
+
+
+
+
+ = date('Y-m-d H:i', strtotime($action['activity_at'])) ?>
+
+ = esc(trim($action['actor_name'] ?? 'System')) ?>
+ = esc($action['actor_email'] ?? '') ?>
+
+ = esc($action['action']) ?>
+ = esc($action['target_user_type'] ?? '-') ?> #= $action['target_user_id'] ?? 'N/A' ?>
+ = esc($action['description']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/admin/activity_log.php b/app/Views/admin/activity_log.php
index 0a68111..196325f 100644
--- a/app/Views/admin/activity_log.php
+++ b/app/Views/admin/activity_log.php
@@ -8,8 +8,75 @@
+
+