modify the dashboard
This commit is contained in:
parent
52a7120edf
commit
996ff00fd7
@ -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');
|
||||||
|
|||||||
28
app/Controllers/ActivityLog.php
Normal file
28
app/Controllers/ActivityLog.php
Normal 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
app/Models/ActivityLogModel.php
Normal file
79
app/Models/ActivityLogModel.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
149
app/Views/admin/activity_log.php
Normal file
149
app/Views/admin/activity_log.php
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)">
|
||||||
</a>
|
<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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -76,34 +78,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<?= 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>
|
||||||
|
|||||||
128
app/Views/components/password_field.php
Normal file
128
app/Views/components/password_field.php
Normal 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>
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Loading…
x
Reference in New Issue
Block a user