modify the dashboard

This commit is contained in:
Sayan Das 2026-04-14 18:42:34 +05:30
parent 52a7120edf
commit 996ff00fd7
24 changed files with 765 additions and 262 deletions

View File

@ -46,3 +46,4 @@ $routes->get('/admin/dashboard', 'Admin::dashboard');
$routes->post('/check-email', 'Admin::checkEmail'); $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');

View File

@ -0,0 +1,28 @@
<?php
namespace App\Controllers;
use App\Models\ActivityLogModel;
class ActivityLog extends BaseController
{
public function index()
{
if ($r = $this->requireRole('admin')) {
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')),
];
return view('admin/activity_log', [
'logs' => $logModel->getFiltered($filters),
'filters' => $filters,
]);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Controllers; namespace App\Controllers;
use App\Models\ActivityLogModel;
use App\Models\UserModel; use App\Models\UserModel;
use App\Models\DoctorModel; use App\Models\DoctorModel;
use App\Models\DoctorSpecializationModel; use App\Models\DoctorSpecializationModel;
@ -107,6 +108,24 @@ class Admin extends BaseController
]; ];
} }
private function validateDobInput(): bool
{
$dob = trim((string) $this->request->getPost('dob'));
if ($dob === '') {
return true;
}
$date = \DateTime::createFromFormat('Y-m-d', $dob);
$errors = \DateTime::getLastErrors();
if (! $date || ($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0 || $date->format('Y-m-d') !== $dob) {
return false;
}
return $dob <= date('Y-m-d');
}
private function getDoctorFormData(int $userId): ?array private function getDoctorFormData(int $userId): ?array
{ {
$userModel = new UserModel(); $userModel = new UserModel();
@ -405,6 +424,9 @@ class Admin extends BaseController
return redirect()->back()->withInput()->with('error', 'Transaction failed.'); return redirect()->back()->withInput()->with('error', 'Transaction failed.');
} }
$logModel = new ActivityLogModel();
$logModel->log('create_doctor', "Admin created doctor {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/doctors'))->with('success', 'Doctor account created. Generated password: ' . $generatedPassword); return redirect()->to(site_url('admin/doctors'))->with('success', 'Doctor account created. Generated password: ' . $generatedPassword);
} }
@ -419,7 +441,7 @@ class Admin extends BaseController
'last_name' => 'required|min_length[2]|max_length[50]|alpha_space', 'last_name' => 'required|min_length[2]|max_length[50]|alpha_space',
'email' => 'required|valid_email|is_unique[users.email]', 'email' => 'required|valid_email|is_unique[users.email]',
'phone' => 'required|min_length[10]|max_length[15]', 'phone' => 'required|min_length[10]|max_length[15]',
'age' => 'permit_empty|integer|greater_than_equal_to[0]|less_than_equal_to[120]', 'dob' => 'permit_empty|valid_date[Y-m-d]',
'gender' => 'permit_empty|in_list[male,female,other]', 'gender' => 'permit_empty|in_list[male,female,other]',
]; ];
@ -427,6 +449,10 @@ class Admin extends BaseController
return redirect()->back()->withInput(); return redirect()->back()->withInput();
} }
if (! $this->validateDobInput()) {
return redirect()->back()->withInput()->with('error', 'Please enter a valid date of birth that is not in the future.');
}
$userModel = new UserModel(); $userModel = new UserModel();
$patientModel = new PatientModel(); $patientModel = new PatientModel();
$db = \Config\Database::connect(); $db = \Config\Database::connect();
@ -434,7 +460,7 @@ class Admin extends BaseController
$firstName = trim((string) $this->request->getPost('first_name')); $firstName = trim((string) $this->request->getPost('first_name'));
$lastName = trim((string) $this->request->getPost('last_name')); $lastName = trim((string) $this->request->getPost('last_name'));
$generatedPassword = $this->generateAccountPassword(); $generatedPassword = $this->generateAccountPassword();
$ageRaw = (string) $this->request->getPost('age'); $dobRaw = trim((string) $this->request->getPost('dob'));
$db->transStart(); $db->transStart();
@ -457,7 +483,7 @@ class Admin extends BaseController
$patientRow = [ $patientRow = [
'user_id' => $userId, 'user_id' => $userId,
'age' => $ageRaw !== '' ? (int) $ageRaw : null, 'dob' => $dobRaw !== '' ? $dobRaw : null,
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null, 'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
'phone' => trim((string) $this->request->getPost('phone')), 'phone' => trim((string) $this->request->getPost('phone')),
]; ];
@ -474,6 +500,9 @@ class Admin extends BaseController
return redirect()->back()->withInput()->with('error', 'Transaction failed.'); return redirect()->back()->withInput()->with('error', 'Transaction failed.');
} }
$logModel = new ActivityLogModel();
$logModel->log('create_patient', "Admin created patient {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/patients'))->with('success', 'Patient account created. Generated password: ' . $generatedPassword); return redirect()->to(site_url('admin/patients'))->with('success', 'Patient account created. Generated password: ' . $generatedPassword);
} }
@ -556,6 +585,9 @@ class Admin extends BaseController
return redirect()->back()->withInput()->with('error', 'Could not update doctor.'); return redirect()->back()->withInput()->with('error', 'Could not update doctor.');
} }
$logModel = new ActivityLogModel();
$logModel->log('update_doctor', "Admin updated doctor {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/doctors'))->with('success', 'Doctor updated successfully.'); return redirect()->to(site_url('admin/doctors'))->with('success', 'Doctor updated successfully.');
} }
@ -581,7 +613,7 @@ class Admin extends BaseController
'last_name' => 'required|min_length[2]|max_length[50]|alpha_space', 'last_name' => 'required|min_length[2]|max_length[50]|alpha_space',
'email' => 'required|valid_email|is_unique[users.email,id,' . $userId . ']', 'email' => 'required|valid_email|is_unique[users.email,id,' . $userId . ']',
'phone' => 'required|min_length[10]|max_length[15]', 'phone' => 'required|min_length[10]|max_length[15]',
'age' => 'permit_empty|integer|greater_than_equal_to[0]|less_than_equal_to[120]', 'dob' => 'permit_empty|valid_date[Y-m-d]',
'gender' => 'permit_empty|in_list[male,female,other]', 'gender' => 'permit_empty|in_list[male,female,other]',
]; ];
@ -589,13 +621,17 @@ class Admin extends BaseController
return redirect()->back()->withInput(); return redirect()->back()->withInput();
} }
if (! $this->validateDobInput()) {
return redirect()->back()->withInput()->with('error', 'Please enter a valid date of birth that is not in the future.');
}
$userModel = new UserModel(); $userModel = new UserModel();
$patientModel = new PatientModel(); $patientModel = new PatientModel();
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$firstName = trim((string) $this->request->getPost('first_name')); $firstName = trim((string) $this->request->getPost('first_name'));
$lastName = trim((string) $this->request->getPost('last_name')); $lastName = trim((string) $this->request->getPost('last_name'));
$ageRaw = (string) $this->request->getPost('age'); $dobRaw = trim((string) $this->request->getPost('dob'));
$db->transStart(); $db->transStart();
@ -606,7 +642,7 @@ class Admin extends BaseController
]); ]);
$patientModel->update($patientData['patient']['id'], [ $patientModel->update($patientData['patient']['id'], [
'age' => $ageRaw !== '' ? (int) $ageRaw : null, 'dob' => $dobRaw !== '' ? $dobRaw : null,
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null, 'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
'phone' => trim((string) $this->request->getPost('phone')), 'phone' => trim((string) $this->request->getPost('phone')),
]); ]);
@ -617,6 +653,9 @@ class Admin extends BaseController
return redirect()->back()->withInput()->with('error', 'Could not update patient.'); return redirect()->back()->withInput()->with('error', 'Could not update patient.');
} }
$logModel = new ActivityLogModel();
$logModel->log('update_patient', "Admin updated patient {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/patients'))->with('success', 'Patient updated successfully.'); return redirect()->to(site_url('admin/patients'))->with('success', 'Patient updated successfully.');
} }
public function checkEmail() public function checkEmail()

View File

@ -2,6 +2,7 @@
namespace App\Controllers; namespace App\Controllers;
use App\Models\ActivityLogModel;
use App\Models\UserModel; use App\Models\UserModel;
use App\Models\PatientModel; use App\Models\PatientModel;
@ -56,6 +57,9 @@ class Auth extends BaseController
'phone' => '+91' . $this->request->getPost('phone'), 'phone' => '+91' . $this->request->getPost('phone'),
]); ]);
$logModel = new ActivityLogModel();
$logModel->log('register_patient', "Patient account registered: {$firstName} {$lastName}", 'user', (int) $user_id);
return redirect()->to(site_url('/'))->with('success', 'Account created. You can log in now.'); return redirect()->to(site_url('/'))->with('success', 'Account created. You can log in now.');
} }
@ -91,6 +95,9 @@ class Auth extends BaseController
'login_token' => $loginToken, 'login_token' => $loginToken,
]); ]);
$logModel = new ActivityLogModel();
$logModel->log('login', "User logged in as {$user['role']}", 'user', (int) $user['id']);
if ($user['role'] === 'admin') { if ($user['role'] === 'admin') {
return redirect()->to(site_url('admin/dashboard')); return redirect()->to(site_url('admin/dashboard'));
} }
@ -107,8 +114,14 @@ class Auth extends BaseController
public function logout() public function logout()
{ {
$userId = (int) session()->get('id'); $userId = (int) session()->get('id');
$role = (string) session()->get('role');
$token = (string) session()->get('login_token'); $token = (string) session()->get('login_token');
if ($userId > 0) {
$logModel = new ActivityLogModel();
$logModel->log('logout', "User logged out from {$role} panel", 'user', $userId);
}
if ($userId > 0 && $token !== '') { if ($userId > 0 && $token !== '') {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$db->table('users') $db->table('users')

View File

@ -3,6 +3,7 @@
namespace App\Controllers; namespace App\Controllers;
use App\Models\AppointmentModel; use App\Models\AppointmentModel;
use App\Models\ActivityLogModel;
use App\Models\DoctorModel; use App\Models\DoctorModel;
use App\Models\DoctorSpecializationModel; use App\Models\DoctorSpecializationModel;
use App\Models\SpecializationModel; use App\Models\SpecializationModel;
@ -129,6 +130,9 @@ class Doctor extends BaseController
$doctorSpecializationModel = new DoctorSpecializationModel(); $doctorSpecializationModel = new DoctorSpecializationModel();
$doctorSpecializationModel->syncDoctorSpecializations($doctor['id'], array_values($specializationMap), (int) session()->get('id')); $doctorSpecializationModel->syncDoctorSpecializations($doctor['id'], array_values($specializationMap), (int) session()->get('id'));
$logModel = new ActivityLogModel();
$logModel->log('update_profile', 'Doctor updated profile details', 'doctor', (int) $doctor['id']);
return redirect()->to(site_url('doctor/profile'))->with('success', 'Profile updated.'); return redirect()->to(site_url('doctor/profile'))->with('success', 'Profile updated.');
} }
@ -193,6 +197,9 @@ class Doctor extends BaseController
$status = AppointmentModel::normalizeStatus($status); $status = AppointmentModel::normalizeStatus($status);
$appointmentModel->update($appointmentId, ['status' => $status]); $appointmentModel->update($appointmentId, ['status' => $status]);
$logModel = new ActivityLogModel();
$logModel->log('update_appointment_status', "Doctor changed appointment status to {$status}", 'appointment', $appointmentId);
return redirect()->back()->with('success', 'Appointment updated.'); return redirect()->back()->with('success', 'Appointment updated.');
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Controllers; namespace App\Controllers;
use App\Models\AppointmentModel; use App\Models\AppointmentModel;
use App\Models\ActivityLogModel;
use App\Models\PatientModel; use App\Models\PatientModel;
class Patient extends BaseController class Patient extends BaseController
@ -103,6 +104,10 @@ class Patient extends BaseController
return redirect()->back()->withInput()->with('error', 'Could not book appointment.'); return redirect()->back()->withInput()->with('error', 'Could not book appointment.');
} }
$appointmentId = (int) $appointmentModel->getInsertID();
$logModel = new ActivityLogModel();
$logModel->log('book_appointment', 'Patient requested a new appointment', 'appointment', $appointmentId);
return redirect()->to(site_url('patient/dashboard'))->with('success', 'Appointment requested.'); return redirect()->to(site_url('patient/dashboard'))->with('success', 'Appointment requested.');
} }
} }

View File

@ -40,7 +40,7 @@ class InitAppointmentSchema extends Migration
$this->forge->addField([ $this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'user_id' => ['type' => 'INT', 'unsigned' => true], 'user_id' => ['type' => 'INT', 'unsigned' => true],
'age' => ['type' => 'INT', 'null' => true], 'dob' => ['type' => 'DATE', 'null' => true],
'gender' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], 'gender' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true],
'phone' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true], 'phone' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true],
]); ]);

View File

@ -0,0 +1,42 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class ReplacePatientAgeWithDob extends Migration
{
public function up(): void
{
$fields = $this->db->getFieldNames('patients');
if (in_array('dob', $fields, true) === false) {
$this->forge->addColumn('patients', [
'dob' => ['type' => 'DATE', 'null' => true, 'after' => 'user_id'],
]);
}
$fields = $this->db->getFieldNames('patients');
if (in_array('age', $fields, true)) {
$this->forge->dropColumn('patients', 'age');
}
}
public function down(): void
{
$fields = $this->db->getFieldNames('patients');
if (in_array('age', $fields, true) === false) {
$this->forge->addColumn('patients', [
'age' => ['type' => 'INT', 'null' => true, 'after' => 'user_id'],
]);
}
$fields = $this->db->getFieldNames('patients');
if (in_array('dob', $fields, true)) {
$this->forge->dropColumn('patients', 'dob');
}
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class DropActivityLogs extends Migration
{
public function up(): void
{
if ($this->db->tableExists('activity_logs')) {
$this->forge->dropTable('activity_logs', true);
}
}
public function down(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'user_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'user_role' => [
'type' => 'ENUM',
'constraint' => ['admin', 'doctor', 'patient', 'system'],
'null' => true,
],
'action' => [
'type' => 'VARCHAR',
'constraint' => 100,
],
'description' => [
'type' => 'TEXT',
'null' => true,
],
'target_type' => [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
],
'target_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'ip_address' => [
'type' => 'VARCHAR',
'constraint' => 45,
'null' => true,
],
'user_agent' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => false,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addKey('user_id');
$this->forge->addKey('action');
$this->forge->addKey('created_at');
$this->forge->createTable('activity_logs', true);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateActivityLogs extends Migration
{
public function up(): void
{
if ($this->db->tableExists('activity_logs')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'actor_user_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'actor_role' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true],
'action' => ['type' => 'VARCHAR', 'constraint' => 100],
'description' => ['type' => 'TEXT', 'null' => true],
'target_type' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
'target_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => true],
'user_agent' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'created_at' => ['type' => 'DATETIME'],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('actor_user_id');
$this->forge->addKey('action');
$this->forge->addKey('created_at');
$this->forge->createTable('activity_logs', true);
}
public function down(): void
{
$this->forge->dropTable('activity_logs', true);
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class ActivityLogModel extends Model
{
protected $table = 'activity_logs';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $protectFields = true;
protected $allowedFields = [
'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
{
$request = service('request');
$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'),
]);
}
public function getFiltered(array $filters = []): 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');
if (! empty($filters['action'])) {
$builder->like('al.action', $filters['action']);
}
if (! empty($filters['role'])) {
$builder->where('al.actor_role', $filters['role']);
}
if (! empty($filters['date_from'])) {
$builder->where('DATE(al.created_at) >=', $filters['date_from']);
}
if (! empty($filters['date_to'])) {
$builder->where('DATE(al.created_at) <=', $filters['date_to']);
}
return $builder
->orderBy('al.created_at', 'DESC')
->get()
->getResultArray();
}
public function getRecent(int $limit = 8): array
{
return $this->db->table('activity_logs')
->orderBy('created_at', 'DESC')
->limit($limit)
->get()
->getResultArray();
}
}

View File

@ -78,7 +78,7 @@ class DoctorModel extends Model
public function getLatestDoctors(int $limit = 5): array public function getLatestDoctors(int $limit = 5): array
{ {
return $this->select("TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, ''))) as name, doctors.specialization") return $this->select("COALESCE(NULLIF(users.formatted_user_id, ''), CONCAT('DOC', LPAD(users.id, 7, '0'))) as formatted_user_id, TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, ''))) as name, doctors.specialization")
->join('users', 'users.id = doctors.user_id') ->join('users', 'users.id = doctors.user_id')
->orderBy('doctors.id', 'DESC') ->orderBy('doctors.id', 'DESC')
->findAll($limit); ->findAll($limit);

View File

@ -12,7 +12,7 @@ class PatientModel extends Model
protected $returnType = 'array'; protected $returnType = 'array';
protected $useSoftDeletes = false; protected $useSoftDeletes = false;
protected $protectFields = true; protected $protectFields = true;
protected $allowedFields = ['user_id','age','gender','phone']; protected $allowedFields = ['user_id','dob','gender','phone'];
protected bool $allowEmptyInserts = false; protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true; protected bool $updateOnlyChanged = true;
@ -76,7 +76,7 @@ class PatientModel extends Model
public function getLatestPatients(int $limit = 5): array public function getLatestPatients(int $limit = 5): array
{ {
return $this->select("TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, ''))) as name, patients.phone") return $this->select("CASE WHEN users.formatted_user_id IS NULL OR users.formatted_user_id = '' OR users.formatted_user_id LIKE 'PHY%' THEN CONCAT('PAT', LPAD(users.id, 7, '0')) ELSE users.formatted_user_id END as formatted_user_id, TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, ''))) as name, patients.phone")
->join('users', 'users.id = patients.user_id') ->join('users', 'users.id = patients.user_id')
->orderBy('patients.id', 'DESC') ->orderBy('patients.id', 'DESC')
->findAll($limit); ->findAll($limit);

View File

@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Activity Log</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<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/dashboard.css') ?>">
</head>
<body class="app-body overview-layout">
<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>
<nav class="ov-nav">
<div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<div class="ov-nav__section">Manage</div>
<div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/patients') ?>" class="ov-nav__sublink">Patient List</a>
<a href="<?= base_url('admin/patients/add') ?>" class="ov-nav__sublink">Add Patient</a>
</div>
</div>
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
<a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link active"><i class="bi bi-clipboard-data"></i> Activity Log</a>
</nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside>
<div class="ov-main" id="mainContent">
<header class="ov-topbar">
<div class="d-flex align-items-center">
<button class="ov-toggle-btn" onclick="toggleSidebar()" title="Toggle Sidebar"><i class="bi bi-list" id="toggleIcon"></i></button>
<p class="ov-topbar__title mb-0">Activity Log</p>
</div>
</header>
<main class="ov-content">
<div class="ov-panel mb-4">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Track System Activity</h2>
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-sm btn-outline-secondary px-3">Back to dashboard</a>
</div>
<div class="ov-panel__body">
<form method="get" action="<?= base_url('admin/activity-log') ?>" class="row g-3">
<div class="col-md-3">
<label class="form-label" for="action">Action</label>
<input type="text" class="form-control" id="action" name="action" value="<?= esc($filters['action'] ?? '') ?>" placeholder="login, create_doctor...">
</div>
<div class="col-md-3">
<label class="form-label" for="role">Role</label>
<select class="form-select" id="role" name="role">
<option value="">All roles</option>
<option value="admin" <?= ($filters['role'] ?? '') === 'admin' ? 'selected' : '' ?>>Admin</option>
<option value="doctor" <?= ($filters['role'] ?? '') === 'doctor' ? 'selected' : '' ?>>Doctor</option>
<option value="patient" <?= ($filters['role'] ?? '') === 'patient' ? 'selected' : '' ?>>Patient</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="date_from">From</label>
<input type="date" class="form-control" id="date_from" name="date_from" value="<?= esc($filters['date_from'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label" for="date_to">To</label>
<input type="date" class="form-control" id="date_to" name="date_to" value="<?= esc($filters['date_to'] ?? '') ?>">
</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>
</form>
</div>
</div>
<div class="ov-panel">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Entries</h2>
<span class="badge bg-light text-dark border"><?= count($logs) ?> records</span>
</div>
<div class="ov-panel__body p-0">
<div class="table-responsive">
<table class="table ov-mini-table mb-0">
<thead>
<tr>
<th class="ps-3">Time</th>
<th>Actor</th>
<th>Role</th>
<th>Action</th>
<th>Description</th>
<th>Target</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<?php if ($logs === []): ?>
<tr>
<td colspan="7" class="ps-3 text-muted">No activity found.</td>
</tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td class="ps-3 text-nowrap"><?= esc($log['created_at']) ?></td>
<td>
<div class="fw-medium"><?= esc(trim((string) ($log['actor_name'] ?? ''))) !== '' ? esc(trim((string) $log['actor_name'])) : 'System' ?></div>
<div class="text-muted small"><?= esc($log['actor_email'] ?? '') ?></div>
</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>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
<script>
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('mainContent');
const icon = document.getElementById('toggleIcon');
sidebar.classList.toggle('collapsed');
main.classList.toggle('expanded');
icon.className = sidebar.classList.contains('collapsed') ? 'bi bi-layout-sidebar' : 'bi bi-list';
}
</script>
</body>
</html>

View File

@ -28,7 +28,16 @@ if (! is_array($oldSpecializations)) {
<div class="ov-nav__section">Main</div> <div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a> <a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<div class="ov-nav__section">Manage</div> <div class="ov-nav__section">Manage</div>
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__link"><i class="bi bi-person-badge"></i> Doctors</a> <div class="ov-nav__dropdown active">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown"> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)"> <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span> <span><i class="bi bi-people"></i> Patients</span>
@ -40,7 +49,7 @@ if (! is_array($oldSpecializations)) {
</div> </div>
</div> </div>
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a> <a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__link active"><i class="bi bi-person-plus"></i> Add Doctor</a> <a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link"><i class="bi bi-clipboard-data"></i> Activity Log</a>
</nav> </nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div> <div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside> </aside>

View File

@ -7,6 +7,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<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') ?>">
<link rel="stylesheet" href="<?= base_url('css/add_doctor.css') ?>"> <link rel="stylesheet" href="<?= base_url('css/add_doctor.css') ?>">
@ -24,7 +25,16 @@
<div class="ov-nav__section">Main</div> <div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a> <a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<div class="ov-nav__section">Manage</div> <div class="ov-nav__section">Manage</div>
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__link"><i class="bi bi-person-badge"></i> Doctors</a> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown active"> <div class="ov-nav__dropdown active">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)"> <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span> <span><i class="bi bi-people"></i> Patients</span>
@ -36,7 +46,7 @@
</div> </div>
</div> </div>
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a> <a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__link"><i class="bi bi-person-plus"></i> Add Doctor</a> <a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link"><i class="bi bi-clipboard-data"></i> Activity Log</a>
</nav> </nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div> <div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside> </aside>
@ -113,39 +123,17 @@
</div> </div>
<?= validation_show_error('phone') ?> <?= validation_show_error('phone') ?>
</div> </div>
<div class="mb-3"> <div class="col-12">
<label class="form-label">Password <span class="text-danger">*</span></label> <?= view('components/password_field', ['id' => 'password']) ?>
</div>
<div class="position-relative">
<input type="password" id="password" name="password"
class="form-control pe-5"
placeholder="Enter strong password"
required>
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
style="cursor:pointer;"
onclick="togglePassword()">
<i id="eyeIcon" class="fa fa-eye"></i>
</span>
</div>
<!-- Password Strength Text -->
<small id="strengthText" class="mt-2 d-block"></small>
<!-- Rules -->
<ul class="small mt-2" id="passwordRules">
<li id="length" class="text-danger">At least 8 characters</li>
<li id="uppercase" class="text-danger">One uppercase letter</li>
<li id="lowercase" class="text-danger">One lowercase letter</li>
<li id="number" class="text-danger">One number</li>
<li id="special" class="text-danger">One special character</li>
</ul>
</div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="age">Age</label> <label class="form-label" for="dob">Date of Birth</label>
<input type="number" name="age" id="age" value="<?= esc(old('age', $patient['age'] ?? '')) ?>" class="form-control <?= isset($validationErrors['age']) ? 'is-invalid' : '' ?>" min="0" max="120" placeholder="Enter age"> <input type="date" name="dob" id="dob"
<?= validation_show_error('age') ?> value="<?= esc(old('dob', $patient['dob'] ?? '')) ?>"
class="form-control <?= isset($validationErrors['dob']) ? 'is-invalid' : '' ?>"
max="<?= date('Y-m-d') ?>">
<?= validation_show_error('dob') ?>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -218,82 +206,6 @@ function checkEmail() {
}) })
.catch(error => console.log(error)); .catch(error => console.log(error));
} }
const password = document.getElementById("password");
password.addEventListener("input", function () {
const val = password.value;
const length = document.getElementById("length");
const upper = document.getElementById("uppercase");
const lower = document.getElementById("lowercase");
const number = document.getElementById("number");
const special = document.getElementById("special");
const strengthText = document.getElementById("strengthText");
let strength = 0;
// Rules check
if (val.length >= 8) {
length.classList.replace("text-danger", "text-success");
strength++;
} else {
length.classList.replace("text-success", "text-danger");
}
if (/[A-Z]/.test(val)) {
upper.classList.replace("text-danger", "text-success");
strength++;
} else {
upper.classList.replace("text-success", "text-danger");
}
if (/[a-z]/.test(val)) {
lower.classList.replace("text-danger", "text-success");
strength++;
} else {
lower.classList.replace("text-success", "text-danger");
}
if (/[0-9]/.test(val)) {
number.classList.replace("text-danger", "text-success");
strength++;
} else {
number.classList.replace("text-success", "text-danger");
}
if (/[^A-Za-z0-9]/.test(val)) {
special.classList.replace("text-danger", "text-success");
strength++;
} else {
special.classList.replace("text-success", "text-danger");
}
// Strength display
if (strength <= 2) {
strengthText.innerHTML = "Weak Password ❌";
strengthText.className = "text-danger";
} else if (strength <= 4) {
strengthText.innerHTML = "Medium Password ⚠️";
strengthText.className = "text-warning";
} else {
strengthText.innerHTML = "Strong Password ✅";
strengthText.className = "text-success";
}
});
function togglePassword() {
const password = document.getElementById("password");
const icon = document.getElementById("eyeIcon");
if (password.type === "password") {
password.type = "text";
icon.classList.remove("fa-eye");
icon.classList.add("fa-eye-slash");
} else {
password.type = "password";
icon.classList.remove("fa-eye-slash");
icon.classList.add("fa-eye");
}
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -16,7 +16,16 @@
<div class="ov-nav__section">Main</div> <div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a> <a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<div class="ov-nav__section">Manage</div> <div class="ov-nav__section">Manage</div>
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__link"><i class="bi bi-person-badge"></i> Doctors</a> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown"> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)"> <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span> <span><i class="bi bi-people"></i> Patients</span>
@ -28,7 +37,7 @@
</div> </div>
</div> </div>
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link active"><i class="bi bi-calendar2-check"></i> Appointments</a> <a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link active"><i class="bi bi-calendar2-check"></i> Appointments</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__link"><i class="bi bi-person-plus"></i> Add Doctor</a> <a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link"><i class="bi bi-clipboard-data"></i> Activity Log</a>
</nav> </nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div> <div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside> </aside>

View File

@ -64,8 +64,10 @@
<title>Admin Dashboard</title> <title>Admin Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<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="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.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') ?>">
<link rel="stylesheet" href="<?= base_url('css/doctors.css') ?>">
</head> </head>
<body class="app-body overview-layout"> <body class="app-body overview-layout">
@ -83,9 +85,16 @@
</a> </a>
<div class="ov-nav__section">Manage</div> <div class="ov-nav__section">Manage</div>
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__link"> <div class="ov-nav__dropdown">
<i class="bi bi-person-badge"></i> Doctors <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a> </a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown"> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)"> <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span> <span><i class="bi bi-people"></i> Patients</span>
@ -99,8 +108,8 @@
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"> <a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link">
<i class="bi bi-calendar2-check"></i> Appointments <i class="bi bi-calendar2-check"></i> Appointments
</a> </a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__link"> <a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link">
<i class="bi bi-person-plus"></i> Add Doctor <i class="bi bi-clipboard-data"></i> Activity Log
</a> </a>
</nav> </nav>
@ -227,6 +236,11 @@
<i class="bi bi-calendar2-week"></i> Appointments <i class="bi bi-calendar2-week"></i> Appointments
</a> </a>
</div> </div>
<div class="col-6">
<a href="<?= base_url('admin/activity-log') ?>" class="ov-action">
<i class="bi bi-clipboard-data"></i> Activity Log
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -287,19 +301,19 @@
<a href="<?= base_url('admin/doctors') ?>" class="btn btn-sm btn-outline-secondary px-3" style="font-size:0.78rem;">View all</a> <a href="<?= base_url('admin/doctors') ?>" class="btn btn-sm btn-outline-secondary px-3" style="font-size:0.78rem;">View all</a>
</div> </div>
<div class="p-0"> <div class="p-0">
<table class="table ov-mini-table mb-0"> <table id="dashboardDoctorsTable" class="table ov-mini-table mb-0">
<thead> <thead>
<tr> <tr>
<th class="ps-3">#</th> <th class="ps-3">#User ID</th>
<th>Name</th> <th>Name</th>
<th>Specialization</th> <th>Specialization</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (!empty($latestDoctors)) : ?> <?php if (!empty($latestDoctors)) : ?>
<?php foreach ($latestDoctors as $i => $doctor) : ?> <?php foreach ($latestDoctors as $doctor) : ?>
<tr> <tr>
<td class="ps-3"><?= $i + 1 ?></td> <td class="ps-3"><?= esc($doctor['formatted_user_id'] ?? 'N/A') ?></td>
<td>Dr. <?= esc($doctor['name']) ?></td> <td>Dr. <?= esc($doctor['name']) ?></td>
<td><?= esc($doctor['specialization']) ?></td> <td><?= esc($doctor['specialization']) ?></td>
</tr> </tr>
@ -320,19 +334,19 @@
<a href="<?= base_url('admin/patients') ?>" class="btn btn-sm btn-outline-secondary px-3" style="font-size:0.78rem;">View all</a> <a href="<?= base_url('admin/patients') ?>" class="btn btn-sm btn-outline-secondary px-3" style="font-size:0.78rem;">View all</a>
</div> </div>
<div class="p-0"> <div class="p-0">
<table class="table ov-mini-table mb-0"> <table id="dashboardPatientsTable" class="table ov-mini-table mb-0">
<thead> <thead>
<tr> <tr>
<th class="ps-3">#</th> <th class="ps-3">#User ID</th>
<th>Name</th> <th>Name</th>
<th>Phone</th> <th>Phone</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (!empty($latestPatients)) : ?> <?php if (!empty($latestPatients)) : ?>
<?php foreach ($latestPatients as $i => $patient) : ?> <?php foreach ($latestPatients as $patient) : ?>
<tr> <tr>
<td class="ps-3"><?= $i + 1 ?></td> <td class="ps-3"><?= esc($patient['formatted_user_id'] ?? 'N/A') ?></td>
<td><?= esc($patient['name']) ?></td> <td><?= esc($patient['name']) ?></td>
<td><?= esc($patient['phone']) ?></td> <td><?= esc($patient['phone']) ?></td>
</tr> </tr>
@ -351,6 +365,9 @@
</main> </main>
</div> </div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// Sidebar toggle // Sidebar toggle
@ -385,6 +402,28 @@
document.getElementById('profileDropdown').classList.remove('open'); document.getElementById('profileDropdown').classList.remove('open');
} }
}); });
$(document).ready(function () {
$('#dashboardDoctorsTable').DataTable({
paging: false,
searching: false,
info: false,
lengthChange: false,
ordering: true,
order: [[0, 'asc']],
autoWidth: false
});
$('#dashboardPatientsTable').DataTable({
paging: false,
searching: false,
info: false,
lengthChange: false,
ordering: true,
order: [[0, 'asc']],
autoWidth: false
});
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -22,7 +22,16 @@
<div class="ov-nav__section">Main</div> <div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a> <a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<div class="ov-nav__section">Manage</div> <div class="ov-nav__section">Manage</div>
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__link active"><i class="bi bi-person-badge"></i> Doctors</a> <div class="ov-nav__dropdown active">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown"> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)"> <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span> <span><i class="bi bi-people"></i> Patients</span>
@ -34,7 +43,7 @@
</div> </div>
</div> </div>
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a> <a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__link"><i class="bi bi-person-plus"></i> Add Doctor</a> <a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link"><i class="bi bi-clipboard-data"></i> Activity Log</a>
</nav> </nav>
<div class="ov-sidebar__footer"> <div class="ov-sidebar__footer">
<a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a> <a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a>

View File

@ -19,7 +19,16 @@
<div class="ov-nav__section">Main</div> <div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a> <a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<div class="ov-nav__section">Manage</div> <div class="ov-nav__section">Manage</div>
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__link"><i class="bi bi-person-badge"></i> Doctors</a> <div class="ov-nav__dropdown">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-person-badge"></i> Doctors</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="ov-dropdown-menu">
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
</div>
</div>
<div class="ov-nav__dropdown active"> <div class="ov-nav__dropdown active">
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)"> <a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
<span><i class="bi bi-people"></i> Patients</span> <span><i class="bi bi-people"></i> Patients</span>
@ -31,7 +40,7 @@
</div> </div>
</div> </div>
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a> <a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__link"><i class="bi bi-person-plus"></i> Add Doctor</a> <a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link"><i class="bi bi-clipboard-data"></i> Activity Log</a>
</nav> </nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div> <div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside> </aside>

View File

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Register</title> <title>Register</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>"> <link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
</head> </head>
@ -77,33 +79,8 @@
<?= validation_show_error('phone') ?> <?= validation_show_error('phone') ?>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Password <span class="text-danger">*</span></label> <?= view('components/password_field', ['id' => 'password']) ?>
</div>
<div class="position-relative">
<input type="password" id="password" name="password"
class="form-control pe-5"
placeholder="Enter strong password"
required>
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
style="cursor:pointer;"
onclick="togglePassword()">
<i id="eyeIcon" class="fa fa-eye"></i>
</span>
</div>
<!-- Password Strength Text -->
<small id="strengthText" class="mt-2 d-block"></small>
<!-- Rules -->
<ul class="small mt-2" id="passwordRules">
<li id="length" class="text-danger">At least 8 characters</li>
<li id="uppercase" class="text-danger">One uppercase letter</li>
<li id="lowercase" class="text-danger">One lowercase letter</li>
<li id="number" class="text-danger">One number</li>
<li id="special" class="text-danger">One special character</li>
</ul>
</div>
<p class="small text-muted mb-4">Register as a <strong>patient</strong> to book appointments.</p> <p class="small text-muted mb-4">Register as a <strong>patient</strong> to book appointments.</p>
@ -116,84 +93,5 @@
</div> </div>
</div> </div>
</div> </div>
<script src="<?= base_url('js/script.js') ?>"></script>
<script>
const password = document.getElementById("password");
password.addEventListener("input", function () {
const val = password.value;
const length = document.getElementById("length");
const upper = document.getElementById("uppercase");
const lower = document.getElementById("lowercase");
const number = document.getElementById("number");
const special = document.getElementById("special");
const strengthText = document.getElementById("strengthText");
let strength = 0;
// Rules check
if (val.length >= 8) {
length.classList.replace("text-danger", "text-success");
strength++;
} else {
length.classList.replace("text-success", "text-danger");
}
if (/[A-Z]/.test(val)) {
upper.classList.replace("text-danger", "text-success");
strength++;
} else {
upper.classList.replace("text-success", "text-danger");
}
if (/[a-z]/.test(val)) {
lower.classList.replace("text-danger", "text-success");
strength++;
} else {
lower.classList.replace("text-success", "text-danger");
}
if (/[0-9]/.test(val)) {
number.classList.replace("text-danger", "text-success");
strength++;
} else {
number.classList.replace("text-success", "text-danger");
}
if (/[^A-Za-z0-9]/.test(val)) {
special.classList.replace("text-danger", "text-success");
strength++;
} else {
special.classList.replace("text-success", "text-danger");
}
// Strength display
if (strength <= 2) {
strengthText.innerHTML = "Weak Password ❌";
strengthText.className = "text-danger";
} else if (strength <= 4) {
strengthText.innerHTML = "Medium Password ⚠️";
strengthText.className = "text-warning";
} else {
strengthText.innerHTML = "Strong Password ✅";
strengthText.className = "text-success";
}
});
function togglePassword() {
const password = document.getElementById("password");
const icon = document.getElementById("eyeIcon");
if (password.type === "password") {
password.type = "text";
icon.classList.remove("fa-eye");
icon.classList.add("fa-eye-slash");
} else {
password.type = "password";
icon.classList.remove("fa-eye-slash");
icon.classList.add("fa-eye");
}
}
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,128 @@
<?php
$id = $id ?? 'password';
$name = $name ?? 'password';
$placeholder = $placeholder ?? 'Enter strong password';
?>
<div class="mb-3">
<label class="form-label">
Password <span class="text-danger">*</span>
</label>
<div class="position-relative">
<input type="password" id="<?= $id ?>" name="<?= $name ?>"
class="form-control pe-5 pe-10"
placeholder="<?= $placeholder ?>"
required>
<!-- Info Icon (NEW) -->
<span class="position-absolute top-50 end-0 translate-middle-y me-5"
style="cursor:pointer;"
data-bs-toggle="tooltip"
data-bs-html="true"
title="
<b>Password must contain:</b><br>
One uppercase letter<br>
One lowercase letter<br>
One number<br>
One special character<br>
Minimum 8 characters
">
<i class="fa fa-circle-info text-primary"></i>
</span>
<!-- Eye Icon -->
<span class="position-absolute top-50 end-0 translate-middle-y me-2"
style="cursor:pointer;"
onclick="togglePassword_<?= $id ?>()">
<i id="<?= $id ?>_icon" class="fa fa-eye"></i>
</span>
</div>
<!-- Strength -->
<small id="<?= $id ?>_strength" class="d-block mt-2"></small>
<!-- Error Message -->
<small id="<?= $id ?>_error" class="text-danger d-none"></small>
</div>
<script>
(function () {
const password = document.getElementById("<?= $id ?>");
if (!password) return;
password.addEventListener("input", function () {
const val = password.value;
const strengthText = document.getElementById("<?= $id ?>_strength");
const errorText = document.getElementById("<?= $id ?>_error");
if (val.length === 0) {
strengthText.innerHTML = "";
errorText.classList.add("d-none");
return;
}
let strength = 0;
let missing = [];
if (/[A-Z]/.test(val)) strength++;
else missing.push("uppercase letter");
if (/[a-z]/.test(val)) strength++;
else missing.push("lowercase letter");
if (/[0-9]/.test(val)) strength++;
else missing.push("number");
if (/[^A-Za-z0-9]/.test(val)) strength++;
else missing.push("special character");
if (val.length >= 8) strength++;
else missing.push("minimum 8 characters");
// Strength UI
if (strength <= 2) {
strengthText.innerHTML = `Weak Password`;
strengthText.className = "d-block mt-2 text-danger";
} else if (strength <= 4) {
strengthText.innerHTML = `Medium Password`;
strengthText.className = "d-block mt-2 text-warning";
} else {
strengthText.innerHTML = `Strong Password`;
strengthText.className = "d-block mt-2 text-success";
}
// Dynamic Error Message
if (val.length > 0 && missing.length > 0) {
errorText.innerHTML = `Missing: ${missing.join(", ")}`;
errorText.classList.remove("d-none");
} else {
errorText.classList.add("d-none");
}
});
})();
// Toggle Password
function togglePassword_<?= $id ?>() {
const password = document.getElementById("<?= $id ?>");
const icon = document.getElementById("<?= $id ?>_icon");
if (password.type === "password") {
password.type = "text";
icon.classList.replace("fa-eye", "fa-eye-slash");
} else {
password.type = "password";
icon.classList.replace("fa-eye-slash", "fa-eye");
}
}
// Enable Bootstrap tooltip
document.addEventListener("DOMContentLoaded", function () {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (el) {
return new bootstrap.Tooltip(el);
});
});
</script>

View File

@ -9,7 +9,9 @@
} }
#doctorsTable thead th, #doctorsTable thead th,
#patientsTable thead th { #patientsTable thead th,
#dashboardDoctorsTable thead th,
#dashboardPatientsTable thead th {
white-space: nowrap; white-space: nowrap;
} }
@ -22,7 +24,17 @@
#patientsTable.table.dataTable thead .sorting_asc, #patientsTable.table.dataTable thead .sorting_asc,
#patientsTable.table.dataTable thead .sorting_desc, #patientsTable.table.dataTable thead .sorting_desc,
#patientsTable.table.dataTable thead .sorting_asc_disabled, #patientsTable.table.dataTable thead .sorting_asc_disabled,
#patientsTable.table.dataTable thead .sorting_desc_disabled { #patientsTable.table.dataTable thead .sorting_desc_disabled,
#dashboardDoctorsTable.table.dataTable thead .sorting,
#dashboardDoctorsTable.table.dataTable thead .sorting_asc,
#dashboardDoctorsTable.table.dataTable thead .sorting_desc,
#dashboardDoctorsTable.table.dataTable thead .sorting_asc_disabled,
#dashboardDoctorsTable.table.dataTable thead .sorting_desc_disabled,
#dashboardPatientsTable.table.dataTable thead .sorting,
#dashboardPatientsTable.table.dataTable thead .sorting_asc,
#dashboardPatientsTable.table.dataTable thead .sorting_desc,
#dashboardPatientsTable.table.dataTable thead .sorting_asc_disabled,
#dashboardPatientsTable.table.dataTable thead .sorting_desc_disabled {
background-position: center right 0.35rem; background-position: center right 0.35rem;
padding-right: 2rem; padding-right: 2rem;
} }

View File

@ -1,14 +1,14 @@
function togglePassword() { // function togglePassword() {
const password = document.getElementById("password"); // const password = document.getElementById("password");
const icon = document.getElementById("eyeIcon"); // const icon = document.getElementById("eyeIcon");
if (password.type === "password") { // if (password.type === "password") {
password.type = "text"; // password.type = "text";
icon.classList.remove("fa-eye"); // icon.classList.remove("fa-eye");
icon.classList.add("fa-eye-slash"); // icon.classList.add("fa-eye-slash");
} else { // } else {
password.type = "password"; // password.type = "password";
icon.classList.remove("fa-eye-slash"); // icon.classList.remove("fa-eye-slash");
icon.classList.add("fa-eye"); // icon.classList.add("fa-eye");
} // }
} // }