This commit is contained in:
Sayan Das 2026-04-17 12:30:45 +05:30
commit ad398e7627
7 changed files with 698 additions and 95 deletions

View File

@ -47,3 +47,8 @@ $routes->post('/check-email', 'Admin::checkEmail');
$routes->get('admin/doctors/data', 'Admin::getDoctors'); $routes->get('admin/doctors/data', 'Admin::getDoctors');
$routes->get('admin/patients/data', 'Admin::getPatients'); $routes->get('admin/patients/data', 'Admin::getPatients');
$routes->get('/admin/activity-log', 'ActivityLog::index'); $routes->get('/admin/activity-log', 'ActivityLog::index');
$routes->get('/admin/activity/analytics', 'ActivityLog::analytics');
$routes->post('/admin/activity-log/clear', 'ActivityLog::clear');
// $routes->post('/admin/activity-log/datatable', 'ActivityLog::datatable');
// $routes->get('/admin/activity-log/summary', 'ActivityLog::getSummary');
// $routes->get('/admin/activity-log/critical', 'ActivityLog::getCritical');

View File

@ -12,17 +12,111 @@ class ActivityLog extends BaseController
return $r; return $r;
} }
$logModel = new ActivityLogModel(); $activityModel = new ActivityLogModel();
$filters = [
'action' => trim((string) $this->request->getGet('action')), // Get filter parameters
'role' => trim((string) $this->request->getGet('role')), $search = $this->request->getGet('search') ?? '';
'date_from' => trim((string) $this->request->getGet('date_from')), $action = $this->request->getGet('action') ?? '';
'date_to' => trim((string) $this->request->getGet('date_to')), $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);
// Debug: Check database connection and count
$db = \Config\Database::connect();
$totalInDb = $db->table('activity_logs')->countAll();
log_message('debug', 'Activity logs in database: ' . $totalInDb . ', retrieved: ' . count($logs));
return view('admin/activity_log', [ return view('admin/activity_log', [
'logs' => $logModel->getFiltered($filters), 'logs' => $logs,
'filters' => $filters, 'totalLogs' => count($logs),
'totalInDb' => $totalInDb,
'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 clear()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
if (strtolower($this->request->getMethod()) === 'post') {
$activityModel = new ActivityLogModel();
if ($activityModel->clearAll()) {
return redirect()->to(base_url('admin/activity-log'))->with('success', 'All activity logs have been cleared.');
} else {
return redirect()->to(base_url('admin/activity-log'))->with('error', 'Failed to clear activity logs.');
}
}
return redirect()->to(base_url('admin/activity-log'));
}
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),
]);
}
} }

View File

@ -96,7 +96,10 @@ class Auth extends BaseController
]); ]);
$logModel = new ActivityLogModel(); $logModel = new ActivityLogModel();
$logModel->log('login', "User logged in as {$user['role']}", 'user', (int) $user['id']); $result = $logModel->log('login', "User logged in as {$user['role']}", 'user', (int) $user['id']);
if (!$result) {
log_message('error', 'Failed to log login activity for user ' . $user['id']);
}
if ($user['role'] === 'admin') { if ($user['role'] === 'admin') {
return redirect()->to(site_url('admin/dashboard')); return redirect()->to(site_url('admin/dashboard'));

View File

@ -0,0 +1,132 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AlterActivityLogsTable extends Migration
{
public function up()
{
// Drop the old table
$this->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');
}
}

View File

@ -10,70 +10,210 @@ class ActivityLogModel extends Model
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $useAutoIncrement = true; protected $useAutoIncrement = true;
protected $returnType = 'array'; protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true; protected $protectFields = true;
protected $allowedFields = [ protected $allowedFields = ['ip', 'action', 'description', 'activity_user_id', 'activity_user_type', 'target_user_id', 'target_user_type', 'activity_page', 'activity_at'];
'actor_user_id',
'actor_role',
'action',
'description',
'target_type',
'target_id',
'ip_address',
'user_agent',
'created_at',
];
public function log(string $action, string $description, ?string $targetType = null, ?int $targetId = null): void protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected array $casts = [];
protected array $castHandlers = [];
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
protected $createdField = 'activity_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
public function log(string $action, ?string $description = null, ?string $targetType = null, ?int $targetId = null, ?int $actorId = null, ?string $actorRole = null, ?string $ipAddress = null, ?string $userAgent = null): bool
{ {
$request = service('request'); $actorId = $actorId ?? session()->get('id');
$actorRole = $actorRole ?? session()->get('role') ?? 'guest';
$this->insert([ $request = service('request');
'actor_user_id' => session()->get('id') ? (int) session()->get('id') : null, $ipAddress = $ipAddress ?? ($request->hasHeader('X-Forwarded-For') ? trim(explode(',', $request->getHeaderLine('X-Forwarded-For'))[0]) : $request->getIPAddress());
'actor_role' => session()->get('role') ?: null, $userAgent = $userAgent ?? ($request->hasHeader('User-Agent') ? $request->getHeaderLine('User-Agent') : null);
$validTargetTypes = ['admin', 'doctor', 'patient'];
$targetUserType = in_array($targetType, $validTargetTypes, true) ? $targetType : null;
$data = [
'ip' => $ipAddress,
'action' => $action, 'action' => $action,
'description' => $description, 'description' => $description,
'target_type' => $targetType, 'activity_user_id' => $actorId,
'target_id' => $targetId, 'activity_user_type' => $actorRole,
'ip_address' => method_exists($request, 'getIPAddress') ? $request->getIPAddress() : null, 'target_user_id' => $targetId,
'user_agent' => method_exists($request, 'getUserAgent') ? $request->getUserAgent()->getAgentString() : null, 'target_user_type' => $targetUserType,
'created_at' => date('Y-m-d H:i:s'), 'activity_page' => $request->getPath(),
]); 'activity_at' => date('Y-m-d H:i:s'),
];
try {
$result = $this->insert($data);
if (!$result) {
log_message('error', 'Failed to insert activity log: ' . json_encode($data) . ' - Errors: ' . json_encode($this->errors()));
}
return (bool) $result;
} catch (\Exception $e) {
log_message('error', 'Exception in ActivityLogModel::log: ' . $e->getMessage());
return false;
}
} }
public function getFiltered(array $filters = []): array public function getFilteredLogs(array $filters = [], int $limit = 200): array
{ {
$builder = $this->db->table('activity_logs al') $builder = $this->select('activity_logs.*, COALESCE(NULLIF(CONCAT(users.first_name, " ", users.last_name), " "), users.email, "Guest") AS actor_name')
->select("al.*, CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, '')) AS actor_name, u.email AS actor_email") ->join('users', 'users.id = activity_logs.activity_user_id', 'left');
->join('users u', 'u.id = al.actor_user_id', 'left');
if (! empty($filters['action'])) { // Apply filters
$builder->like('al.action', $filters['action']); 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'])) { if (!empty($filters['action'])) {
$builder->where('al.actor_role', $filters['role']); $builder->where('activity_logs.action', $filters['action']);
} }
if (! empty($filters['date_from'])) { if (!empty($filters['user_type'])) {
$builder->where('DATE(al.created_at) >=', $filters['date_from']); $builder->where('activity_logs.activity_user_type', $filters['user_type']);
} }
if (! empty($filters['date_to'])) { if (!empty($filters['date_from'])) {
$builder->where('DATE(al.created_at) <=', $filters['date_to']); $builder->where('activity_logs.activity_at >=', $filters['date_from'] . ' 00:00:00');
} }
return $builder if (!empty($filters['date_to'])) {
->orderBy('al.created_at', 'DESC') $builder->where('activity_logs.activity_at <=', $filters['date_to'] . ' 23:59:59');
->get()
->getResultArray();
} }
public function getRecent(int $limit = 8): array if (!empty($filters['ip'])) {
$builder->where('activity_logs.ip', $filters['ip']);
}
return $builder->orderBy('activity_logs.activity_at', 'DESC')
->findAll($limit);
}
public function getAvailableActions(): array
{ {
return $this->db->table('activity_logs') $rows = $this->select('action')
->orderBy('created_at', 'DESC') ->distinct()
->limit($limit) ->orderBy('action', 'ASC')
->get() ->findAll();
->getResultArray();
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);
}
public function clearAll(): bool
{
return $this->truncate();
} }
} }

View File

@ -72,17 +72,26 @@ function formatLabel($label) {
<div class="ov-panel__body"> <div class="ov-panel__body">
<!-- Summary Statistics --> <!-- Summary Statistics -->
<div class="row mb-4"> <div class="row mb-4">
<<<<<<< HEAD
<div class="col-md-3"> <div class="col-md-3">
=======
<div class="col-md-4">
>>>>>>> b13edf05262009091e8d671b88fed19ecf9e1a39
<div class="stat-card"> <div class="stat-card">
<p class="stat-value"><?= $summary['total_actions'] ?? 0 ?></p> <p class="stat-value"><?= $summary['total_actions'] ?? 0 ?></p>
<p class="stat-label">Total Actions</p> <p class="stat-label">Total Actions</p>
</div> </div>
</div> </div>
<<<<<<< HEAD
<div class="col-md-3"> <div class="col-md-3">
=======
<!-- <div class="col-md-3">
>>>>>>> b13edf05262009091e8d671b88fed19ecf9e1a39
<div class="stat-card green"> <div class="stat-card green">
<p class="stat-value"><?= count($summary['by_action'] ?? []) ?></p> <p class="stat-value"><?= count($summary['by_action'] ?? []) ?></p>
<p class="stat-label">Action Types</p> <p class="stat-label">Action Types</p>
</div> </div>
<<<<<<< HEAD
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="stat-card blue"> <div class="stat-card blue">
@ -91,6 +100,16 @@ function formatLabel($label) {
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
=======
</div> -->
<div class="col-md-4">
<div class="stat-card blue">
<p class="stat-value"><?= count($summary['by_role'] ?? []) ?></p>
<p class="stat-label">Activity Roles</p>
</div>
</div>
<div class="col-md-4">
>>>>>>> b13edf05262009091e8d671b88fed19ecf9e1a39
<div class="stat-card orange"> <div class="stat-card orange">
<p class="stat-value"><?= count($summary['most_active_users'] ?? []) ?></p> <p class="stat-value"><?= count($summary['most_active_users'] ?? []) ?></p>
<p class="stat-label">Active Users</p> <p class="stat-label">Active Users</p>
@ -218,11 +237,14 @@ function toggleSidebar() {
icon.className = sidebar.classList.contains('collapsed') ? 'bi bi-layout-sidebar' : 'bi bi-list'; icon.className = sidebar.classList.contains('collapsed') ? 'bi bi-layout-sidebar' : 'bi bi-list';
} }
<<<<<<< HEAD
function toggleNavDropdown(event, element) { function toggleNavDropdown(event, element) {
event.preventDefault(); event.preventDefault();
element.parentElement.classList.toggle('active'); element.parentElement.classList.toggle('active');
} }
=======
>>>>>>> b13edf05262009091e8d671b88fed19ecf9e1a39
function changeAnalyticsPeriod(period) { function changeAnalyticsPeriod(period) {
window.location.href = '<?= base_url('admin/activity/analytics') ?>?period=' + period; window.location.href = '<?= base_url('admin/activity/analytics') ?>?period=' + period;
} }

View File

@ -8,8 +8,75 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>"> <link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
<link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>"> <link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>">
<style>
.stat-card .ov-stat__icon {
background: var(--bs-primary) !important;
color: white !important;
}
.stat-card:nth-child(2) .ov-stat__icon {
background: var(--bs-success) !important;
}
.stat-card:nth-child(3) .ov-stat__icon {
background: var(--bs-info) !important;
}
.avatar-circle {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.avatar-circle.small {
width: 24px;
height: 24px;
font-size: 12px;
}
.description-cell {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-responsive {
border-radius: 0.375rem;
}
.table thead th {
border-bottom: 2px solid #dee2e6;
font-weight: 600;
text-transform: uppercase;
font-size: 0.875rem;
letter-spacing: 0.5px;
}
.badge {
font-weight: 500;
}
.btn-link {
text-decoration: none;
}
.btn-link:hover {
text-decoration: underline;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn:disabled {
opacity: 0.6;
}
#selectedCount {
font-weight: 500;
color: #6c757d;
}
.input-group-text {
background-color: #f8f9fa;
}
</style> <link rel="stylesheet" href="<?= base_url('css/doctors.css') ?>">
</head> </head>
<body class="app-body overview-layout"> <body class="app-body overview-layout">
<aside class="ov-sidebar" id="sidebar"> <aside class="ov-sidebar" id="sidebar">
<div class="ov-brand"><h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1><span>Control Panel</span></div> <div class="ov-brand"><h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1><span>Control Panel</span></div>
<nav class="ov-nav"> <nav class="ov-nav">
@ -51,79 +118,204 @@
</header> </header>
<main class="ov-content"> <main class="ov-content">
<!-- Flash Messages -->
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i><?= esc(session()->getFlashdata('success')) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i><?= esc(session()->getFlashdata('error')) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Search & Filter Section -->
<div class="ov-panel mb-4"> <div class="ov-panel mb-4">
<div class="ov-panel__header"> <div class="ov-panel__header">
<h2 class="ov-panel__title">Track System Activity</h2> <h2 class="ov-panel__title mb-0">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-sm btn-outline-secondary px-3">Back to dashboard</a> <button class="btn btn-link p-0 text-decoration-none" onclick="toggleFilters()" id="filterToggle">
<i class="bi bi-funnel me-2"></i>Search & Filters
<i class="bi bi-chevron-down ms-2" id="filterIcon"></i>
</button>
</h2>
</div> </div>
<div class="ov-panel__body"> <div class="ov-panel__body" id="filterSection" style="display: none;">
<form method="get" action="<?= base_url('admin/activity-log') ?>" class="row g-3"> <form method="GET" action="<?= base_url('admin/activity-log') ?>" class="row g-3">
<div class="col-md-3"> <div class="col-md-4">
<label class="form-label" for="action">Action</label> <label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="action" name="action" value="<?= esc($filters['action'] ?? '') ?>" placeholder="login, create_doctor..."> <input type="text" class="form-control" id="search" name="search"
value="<?= esc($filters['search'] ?? '') ?>"
placeholder="Search by name, email, or description">
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<label class="form-label" for="role">Role</label> <label for="action" class="form-label">Action</label>
<select class="form-select" id="role" name="role"> <select class="form-select" id="action" name="action">
<option value="">All roles</option> <option value="">All Actions</option>
<option value="admin" <?= ($filters['role'] ?? '') === 'admin' ? 'selected' : '' ?>>Admin</option> <?php foreach ($availableActions as $actionType): ?>
<option value="doctor" <?= ($filters['role'] ?? '') === 'doctor' ? 'selected' : '' ?>>Doctor</option> <option value="<?= esc($actionType) ?>" <?= ($filters['action'] ?? '') === $actionType ? 'selected' : '' ?>>
<option value="patient" <?= ($filters['role'] ?? '') === 'patient' ? 'selected' : '' ?>>Patient</option> <?= esc(ucwords(str_replace('_', ' ', $actionType))) ?>
</option>
<?php endforeach; ?>
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label" for="date_from">From</label> <label for="user_type" class="form-label">User Type</label>
<input type="date" class="form-control" id="date_from" name="date_from" value="<?= esc($filters['date_from'] ?? '') ?>"> <select class="form-select" id="user_type" name="user_type">
<option value="">All Types</option>
<option value="admin" <?= ($filters['user_type'] ?? '') === 'admin' ? 'selected' : '' ?>>Admin</option>
<option value="doctor" <?= ($filters['user_type'] ?? '') === 'doctor' ? 'selected' : '' ?>>Doctor</option>
<option value="patient" <?= ($filters['user_type'] ?? '') === 'patient' ? 'selected' : '' ?>>Patient</option>
</select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label" for="date_to">To</label> <label for="date_from" class="form-label">From Date</label>
<input type="date" class="form-control" id="date_to" name="date_to" value="<?= esc($filters['date_to'] ?? '') ?>"> <input type="date" class="form-control" id="date_from" name="date_from"
value="<?= esc($filters['date_from'] ?? '') ?>">
</div>
<div class="col-md-2">
<label for="date_to" class="form-label">To Date</label>
<input type="date" class="form-control" id="date_to" name="date_to"
value="<?= esc($filters['date_to'] ?? '') ?>">
</div>
<div class="col-md-4">
<label for="ip" class="form-label">IP Address</label>
<input type="text" class="form-control" id="ip" name="ip"
value="<?= esc($filters['ip'] ?? '') ?>"
placeholder="Filter by IP address">
</div>
<div class="col-12">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search me-1"></i>Search
</button>
<a href="<?= base_url('admin/activity-log') ?>" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i>Clear Filters
</a>
</div> </div>
<div class="col-md-2 d-flex align-items-end gap-2">
<button type="submit" class="btn btn-app-primary px-4 py-2 w-100">Filter</button>
<a href="<?= base_url('admin/activity-log') ?>" class="btn btn-outline-secondary px-4 py-2 w-100">Reset</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div class="ov-panel"> <div class="row g-3 mb-4">
<div class="ov-panel__header"> <div class="col-md-4">
<h2 class="ov-panel__title">Entries</h2> <div class="ov-stat stat-card">
<span class="badge bg-light text-dark border"><?= count($logs) ?> records</span> <div class="ov-stat__icon bg-primary">
<i class="bi bi-clipboard-data"></i>
</div>
<div>
<div class="ov-stat__label">Total Entries</div>
<p class="ov-stat__value"><?= esc($totalLogs) ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="ov-stat stat-card">
<div class="ov-stat__icon bg-success">
<i class="bi bi-list-check"></i>
</div>
<div>
<div class="ov-stat__label">Action Types</div>
<p class="ov-stat__value"><?= esc(count($actionSummary)) ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="ov-stat stat-card">
<div class="ov-stat__icon bg-info">
<i class="bi bi-people"></i>
</div>
<div>
<div class="ov-stat__label">Actor Roles</div>
<p class="ov-stat__value"><?= esc(count($roleSummary)) ?></p>
</div>
</div>
</div>
</div>
<div class="ov-panel mb-4">
<div class="ov-panel__header d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-2">
<div>
<h2 class="ov-panel__title mb-1">Recent Activity</h2>
<p class="text-muted mb-0">Latest actions recorded from the application.</p>
</div>
<div class="d-flex gap-2">
<a href="<?= base_url('admin/activity/analytics') ?>" class="btn btn-sm btn-outline-secondary">View Analytics</a>
<form method="post" action="<?= base_url('admin/activity-log/clear') ?>" style="display: inline;" onsubmit="return confirm('Are you sure you want to clear all activity logs? This action cannot be undone.')">
<button type="submit" class="btn btn-sm btn-outline-danger">Clear Log</button>
</form>
</div>
</div> </div>
<div class="ov-panel__body p-0"> <div class="ov-panel__body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table ov-mini-table mb-0"> <table class="table table-hover mb-0">
<thead> <thead class="table-light">
<tr> <tr>
<th class="ps-3">Time</th> <th class="text-nowrap">Timestamp</th>
<th>Actor</th> <th>Actor</th>
<th>Role</th> <th>Role</th>
<th>Action</th> <th>Action</th>
<th>Description</th>
<th>Target</th> <th>Target</th>
<th>Description</th>
<th>IP</th> <th>IP</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if ($logs === []): ?> <?php if (empty($logs)): ?>
<tr> <tr>
<td colspan="7" class="ps-3 text-muted">No activity found.</td> <td colspan="7" class="text-center text-muted py-5">
<i class="bi bi-inbox fs-1 d-block mb-2 text-muted"></i>
No activity logs found matching your criteria.
</td>
</tr> </tr>
<?php else: ?> <?php else: ?>
<?php foreach ($logs as $log): ?> <?php foreach ($logs as $log): ?>
<tr> <tr>
<td class="ps-3 text-nowrap"><?= esc($log['created_at']) ?></td> <td class="text-nowrap small">
<td> <div class="d-flex flex-column">
<div class="fw-medium"><?= esc(trim((string) ($log['actor_name'] ?? ''))) !== '' ? esc(trim((string) $log['actor_name'])) : 'System' ?></div> <span class="fw-medium"><?= esc(date('M d, Y', strtotime($log['activity_at']))) ?></span>
<div class="text-muted small"><?= esc($log['actor_email'] ?? '') ?></div> <small class="text-muted"><?= esc(date('H:i:s', strtotime($log['activity_at']))) ?></small>
</div>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-circle me-2 bg-<?= $log['activity_user_type'] === 'admin' ? 'primary' : ($log['activity_user_type'] === 'doctor' ? 'success' : 'info') ?> text-white small">
<?= strtoupper(substr(!empty($log['actor_name']) ? $log['actor_name'] : 'G', 0, 1)) ?>
</div>
<span class="fw-medium"><?= esc($log['actor_name'] ?? 'Guest') ?></span>
</div>
</td>
<td>
<span class="badge bg-<?= $log['activity_user_type'] === 'admin' ? 'primary' : ($log['activity_user_type'] === 'doctor' ? 'success' : 'info') ?> text-uppercase small">
<?= esc($log['activity_user_type'] ?? 'guest') ?>
</span>
</td>
<td>
<span class="badge bg-secondary text-uppercase small">
<i class="bi bi-<?= esc($actionIcons[$log['action']] ?? 'activity') ?> me-1"></i>
<?= esc(str_replace('_', ' ', $log['action'])) ?>
</span>
</td>
<td>
<?php if ($log['target_user_type']): ?>
<span class="badge bg-light text-dark small">
<?= esc($log['target_user_type']) ?> #<?= esc($log['target_user_id']) ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<div class="description-cell">
<?= esc($log['description'] ?? '-') ?>
</div>
</td>
<td class="text-nowrap">
<code class="small bg-light px-2 py-1 rounded"><?= esc($log['ip'] ?? '-') ?></code>
</td> </td>
<td><?= esc($log['actor_role'] ?? '-') ?></td>
<td><span class="badge bg-secondary"><?= esc($log['action']) ?></span></td>
<td><?= esc($log['description']) ?></td>
<td><?= esc(($log['target_type'] ?? '-') . ((isset($log['target_id']) && $log['target_id'] !== null) ? ' #' . $log['target_id'] : '')) ?></td>
<td class="text-nowrap"><?= esc($log['ip_address'] ?? '-') ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
@ -147,8 +339,23 @@ function toggleSidebar() {
function toggleNavDropdown(event, element) { function toggleNavDropdown(event, element) {
event.preventDefault(); event.preventDefault();
<<<<<<< HEAD
element.parentElement.classList.toggle('active'); element.parentElement.classList.toggle('active');
} }
=======
element.parentElement.classList.toggle('open');
}
function toggleFilters() {
const filterSection = document.getElementById('filterSection');
const filterIcon = document.getElementById('filterIcon');
const isVisible = filterSection.style.display !== 'none';
filterSection.style.display = isVisible ? 'none' : 'block';
filterIcon.className = isVisible ? 'bi bi-chevron-down ms-2' : 'bi bi-chevron-up ms-2';
}
>>>>>>> b13edf05262009091e8d671b88fed19ecf9e1a39
</script> </script>
</body> </body>
</html> </html>