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->get('admin/doctors/data', 'Admin::getDoctors');
|
||||
$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;
|
||||
|
||||
use App\Models\ActivityLogModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\DoctorModel;
|
||||
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
|
||||
{
|
||||
$userModel = new UserModel();
|
||||
@ -405,6 +424,9 @@ class Admin extends BaseController
|
||||
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);
|
||||
}
|
||||
|
||||
@ -419,7 +441,7 @@ class Admin extends BaseController
|
||||
'last_name' => 'required|min_length[2]|max_length[50]|alpha_space',
|
||||
'email' => 'required|valid_email|is_unique[users.email]',
|
||||
'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]',
|
||||
];
|
||||
|
||||
@ -427,6 +449,10 @@ class Admin extends BaseController
|
||||
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();
|
||||
$patientModel = new PatientModel();
|
||||
$db = \Config\Database::connect();
|
||||
@ -434,7 +460,7 @@ class Admin extends BaseController
|
||||
$firstName = trim((string) $this->request->getPost('first_name'));
|
||||
$lastName = trim((string) $this->request->getPost('last_name'));
|
||||
$generatedPassword = $this->generateAccountPassword();
|
||||
$ageRaw = (string) $this->request->getPost('age');
|
||||
$dobRaw = trim((string) $this->request->getPost('dob'));
|
||||
|
||||
$db->transStart();
|
||||
|
||||
@ -457,7 +483,7 @@ class Admin extends BaseController
|
||||
|
||||
$patientRow = [
|
||||
'user_id' => $userId,
|
||||
'age' => $ageRaw !== '' ? (int) $ageRaw : null,
|
||||
'dob' => $dobRaw !== '' ? $dobRaw : null,
|
||||
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
|
||||
'phone' => trim((string) $this->request->getPost('phone')),
|
||||
];
|
||||
@ -474,6 +500,9 @@ class Admin extends BaseController
|
||||
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);
|
||||
}
|
||||
|
||||
@ -556,6 +585,9 @@ class Admin extends BaseController
|
||||
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.');
|
||||
}
|
||||
|
||||
@ -581,7 +613,7 @@ class Admin extends BaseController
|
||||
'last_name' => 'required|min_length[2]|max_length[50]|alpha_space',
|
||||
'email' => 'required|valid_email|is_unique[users.email,id,' . $userId . ']',
|
||||
'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]',
|
||||
];
|
||||
|
||||
@ -589,13 +621,17 @@ class Admin extends BaseController
|
||||
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();
|
||||
$patientModel = new PatientModel();
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$firstName = trim((string) $this->request->getPost('first_name'));
|
||||
$lastName = trim((string) $this->request->getPost('last_name'));
|
||||
$ageRaw = (string) $this->request->getPost('age');
|
||||
$dobRaw = trim((string) $this->request->getPost('dob'));
|
||||
|
||||
$db->transStart();
|
||||
|
||||
@ -606,7 +642,7 @@ class Admin extends BaseController
|
||||
]);
|
||||
|
||||
$patientModel->update($patientData['patient']['id'], [
|
||||
'age' => $ageRaw !== '' ? (int) $ageRaw : null,
|
||||
'dob' => $dobRaw !== '' ? $dobRaw : null,
|
||||
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
|
||||
'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.');
|
||||
}
|
||||
|
||||
$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.');
|
||||
}
|
||||
public function checkEmail()
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ActivityLogModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\PatientModel;
|
||||
|
||||
@ -56,6 +57,9 @@ class Auth extends BaseController
|
||||
'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.');
|
||||
}
|
||||
|
||||
@ -91,6 +95,9 @@ class Auth extends BaseController
|
||||
'login_token' => $loginToken,
|
||||
]);
|
||||
|
||||
$logModel = new ActivityLogModel();
|
||||
$logModel->log('login', "User logged in as {$user['role']}", 'user', (int) $user['id']);
|
||||
|
||||
if ($user['role'] === 'admin') {
|
||||
return redirect()->to(site_url('admin/dashboard'));
|
||||
}
|
||||
@ -107,8 +114,14 @@ class Auth extends BaseController
|
||||
public function logout()
|
||||
{
|
||||
$userId = (int) session()->get('id');
|
||||
$role = (string) session()->get('role');
|
||||
$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 !== '') {
|
||||
$db = \Config\Database::connect();
|
||||
$db->table('users')
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\AppointmentModel;
|
||||
use App\Models\ActivityLogModel;
|
||||
use App\Models\DoctorModel;
|
||||
use App\Models\DoctorSpecializationModel;
|
||||
use App\Models\SpecializationModel;
|
||||
@ -129,6 +130,9 @@ class Doctor extends BaseController
|
||||
$doctorSpecializationModel = new DoctorSpecializationModel();
|
||||
$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.');
|
||||
}
|
||||
|
||||
@ -193,6 +197,9 @@ class Doctor extends BaseController
|
||||
$status = AppointmentModel::normalizeStatus($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.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\AppointmentModel;
|
||||
use App\Models\ActivityLogModel;
|
||||
use App\Models\PatientModel;
|
||||
|
||||
class Patient extends BaseController
|
||||
@ -103,6 +104,10 @@ class Patient extends BaseController
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ class InitAppointmentSchema extends Migration
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'user_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'age' => ['type' => 'INT', 'null' => true],
|
||||
'dob' => ['type' => 'DATE', 'null' => true],
|
||||
'gender' => ['type' => 'VARCHAR', 'constraint' => 20, '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
|
||||
{
|
||||
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')
|
||||
->orderBy('doctors.id', 'DESC')
|
||||
->findAll($limit);
|
||||
|
||||
@ -12,7 +12,7 @@ class PatientModel extends Model
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['user_id','age','gender','phone'];
|
||||
protected $allowedFields = ['user_id','dob','gender','phone'];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
@ -76,7 +76,7 @@ class PatientModel extends Model
|
||||
|
||||
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')
|
||||
->orderBy('patients.id', 'DESC')
|
||||
->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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@ -40,7 +49,7 @@ if (! is_array($oldSpecializations)) {
|
||||
</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/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>
|
||||
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
|
||||
</aside>
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
<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://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/dashboard.css') ?>">
|
||||
<link rel="stylesheet" href="<?= base_url('css/add_doctor.css') ?>">
|
||||
@ -24,7 +25,16 @@
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@ -36,7 +46,7 @@
|
||||
</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/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>
|
||||
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
|
||||
</aside>
|
||||
@ -113,39 +123,17 @@
|
||||
</div>
|
||||
<?= validation_show_error('phone') ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password <span class="text-danger">*</span></label>
|
||||
|
||||
<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-12">
|
||||
<?= view('components/password_field', ['id' => 'password']) ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="age">Age</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">
|
||||
<?= validation_show_error('age') ?>
|
||||
<label class="form-label" for="dob">Date of Birth</label>
|
||||
<input type="date" name="dob" id="dob"
|
||||
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 class="col-md-6">
|
||||
@ -218,82 +206,6 @@ function checkEmail() {
|
||||
})
|
||||
.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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -16,7 +16,16 @@
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@ -28,7 +37,7 @@
|
||||
</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/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>
|
||||
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
|
||||
</aside>
|
||||
|
||||
@ -64,8 +64,10 @@
|
||||
<title>Admin Dashboard</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="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/dashboard.css') ?>">
|
||||
<link rel="stylesheet" href="<?= base_url('css/doctors.css') ?>">
|
||||
</head>
|
||||
<body class="app-body overview-layout">
|
||||
|
||||
@ -83,9 +85,16 @@
|
||||
</a>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
@ -99,8 +108,8 @@
|
||||
<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 href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link">
|
||||
<i class="bi bi-clipboard-data"></i> Activity Log
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@ -227,6 +236,11 @@
|
||||
<i class="bi bi-calendar2-week"></i> Appointments
|
||||
</a>
|
||||
</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>
|
||||
@ -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>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<table class="table ov-mini-table mb-0">
|
||||
<table id="dashboardDoctorsTable" class="table ov-mini-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-3">#</th>
|
||||
<th class="ps-3">#User ID</th>
|
||||
<th>Name</th>
|
||||
<th>Specialization</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($latestDoctors)) : ?>
|
||||
<?php foreach ($latestDoctors as $i => $doctor) : ?>
|
||||
<?php foreach ($latestDoctors as $doctor) : ?>
|
||||
<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><?= esc($doctor['specialization']) ?></td>
|
||||
</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>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<table class="table ov-mini-table mb-0">
|
||||
<table id="dashboardPatientsTable" class="table ov-mini-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-3">#</th>
|
||||
<th class="ps-3">#User ID</th>
|
||||
<th>Name</th>
|
||||
<th>Phone</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($latestPatients)) : ?>
|
||||
<?php foreach ($latestPatients as $i => $patient) : ?>
|
||||
<?php foreach ($latestPatients as $patient) : ?>
|
||||
<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['phone']) ?></td>
|
||||
</tr>
|
||||
@ -351,6 +365,9 @@
|
||||
</main>
|
||||
</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>
|
||||
// Sidebar toggle
|
||||
@ -385,6 +402,28 @@
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -22,7 +22,16 @@
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@ -34,7 +43,7 @@
|
||||
</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/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>
|
||||
<div class="ov-sidebar__footer">
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@ -31,7 +40,7 @@
|
||||
</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/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>
|
||||
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
|
||||
</aside>
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Register</title>
|
||||
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
</head>
|
||||
@ -76,34 +78,9 @@
|
||||
</div>
|
||||
<?= validation_show_error('phone') ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password <span class="text-danger">*</span></label>
|
||||
|
||||
<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="mb-3">
|
||||
<?= view('components/password_field', ['id' => 'password']) ?>
|
||||
</div>
|
||||
|
||||
<p class="small text-muted mb-4">Register as a <strong>patient</strong> to book appointments.</p>
|
||||
|
||||
@ -116,84 +93,5 @@
|
||||
</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>
|
||||
</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,
|
||||
#patientsTable thead th {
|
||||
#patientsTable thead th,
|
||||
#dashboardDoctorsTable thead th,
|
||||
#dashboardPatientsTable thead th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@ -22,7 +24,17 @@
|
||||
#patientsTable.table.dataTable thead .sorting_asc,
|
||||
#patientsTable.table.dataTable thead .sorting_desc,
|
||||
#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;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
function togglePassword() {
|
||||
const password = document.getElementById("password");
|
||||
const icon = document.getElementById("eyeIcon");
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
// 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");
|
||||
// }
|
||||
// }
|
||||
Loading…
x
Reference in New Issue
Block a user