Compare commits

...

4 Commits

Author SHA1 Message Date
Sayan Das
996ff00fd7 modify the dashboard 2026-04-14 18:42:34 +05:30
Sayan Das
52a7120edf Updated folder structure 2026-04-13 19:04:12 +05:30
5196c74555 update 2026-04-06 12:03:19 +05:30
bd64fffa16 Added dashboard template sample 2026-04-06 11:53:01 +05:30
47 changed files with 4882 additions and 678 deletions

View File

@ -88,5 +88,5 @@ class Autoload extends AutoloadConfig
*
* @var list<string>
*/
public $helpers = ['form', 'url'];
public $helpers = ['form', 'url', 'encryption'];
}

View File

@ -16,7 +16,13 @@ $routes->get('/admin/dashboard', 'Admin::dashboard');
$routes->get('/admin/doctors', 'Admin::doctors');
$routes->get('/admin/doctors/add', 'Admin::addDoctor');
$routes->post('/admin/doctors/add', 'Admin::storeDoctor');
$routes->get('/admin/doctors/edit/(:any)', 'Admin::editDoctor/$1');
$routes->post('/admin/doctors/edit/(:any)', 'Admin::updateDoctor/$1');
$routes->get('/admin/patients', 'Admin::patients');
$routes->get('/admin/patients/add', 'Admin::addPatient');
$routes->post('/admin/patients/add', 'Admin::storePatient');
$routes->get('/admin/patients/edit/(:any)', 'Admin::editPatient/$1');
$routes->post('/admin/patients/edit/(:any)', 'Admin::updatePatient/$1');
$routes->get('/admin/appointments', 'Admin::appointments');
$routes->get('/admin/deleteDoctor/(:num)', 'Admin::deleteDoctor/$1');
@ -37,3 +43,7 @@ $routes->get('/reset-password/(:any)', 'Auth::resetPassword/$1');
$routes->post('/reset-password', 'Auth::processResetPassword');
$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');

View File

@ -0,0 +1,28 @@
<?php
namespace App\Controllers;
use App\Models\ActivityLogModel;
class ActivityLog extends BaseController
{
public function index()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$logModel = new ActivityLogModel();
$filters = [
'action' => trim((string) $this->request->getGet('action')),
'role' => trim((string) $this->request->getGet('role')),
'date_from' => trim((string) $this->request->getGet('date_from')),
'date_to' => trim((string) $this->request->getGet('date_to')),
];
return view('admin/activity_log', [
'logs' => $logModel->getFiltered($filters),
'filters' => $filters,
]);
}
}

View File

@ -2,13 +2,160 @@
namespace App\Controllers;
use App\Models\ActivityLogModel;
use App\Models\UserModel;
use App\Models\DoctorModel;
use App\Models\DoctorSpecializationModel;
use App\Models\PatientModel;
use App\Models\AppointmentModel;
use App\Models\SpecializationModel;
class Admin extends BaseController
{
private function formatFullName(?string $firstName, ?string $lastName, string $fallback = ''): string
{
$fullName = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
return $fullName !== '' ? $fullName : $fallback;
}
private function generateAccountPassword(int $length = 12): string
{
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
$maxIndex = strlen($alphabet) - 1;
$password = '';
for ($i = 0; $i < $length; $i++) {
$password .= $alphabet[random_int(0, $maxIndex)];
}
return $password;
}
private function parseSpecializations($specializationInput): array
{
$specializations = [];
if (is_array($specializationInput)) {
foreach ($specializationInput as $item) {
$item = trim((string) $item);
if ($item !== '' && ! in_array($item, $specializations, true)) {
$specializations[] = $item;
}
}
} else {
$single = trim((string) $specializationInput);
if ($single !== '') {
$specializations[] = $single;
}
}
return $specializations;
}
private function buildExperienceString(?int $years, ?int $months): ?string
{
$experienceParts = [];
if ($years !== null && $years > 0) {
$experienceParts[] = $years . ' year' . ($years === 1 ? '' : 's');
}
if ($months !== null && $months > 0) {
$experienceParts[] = $months . ' month' . ($months === 1 ? '' : 's');
}
return $experienceParts !== [] ? implode(' ', $experienceParts) : null;
}
private function parseExperienceString(?string $experience): array
{
$experience = trim((string) $experience);
$years = 0;
$months = 0;
if ($experience !== '' && preg_match('/(\d+)\s+year/i', $experience, $yearMatch)) {
$years = (int) $yearMatch[1];
}
if ($experience !== '' && preg_match('/(\d+)\s+month/i', $experience, $monthMatch)) {
$months = (int) $monthMatch[1];
}
return [
'experience_years' => $years,
'experience_months' => $months,
];
}
private function getPatientFormData(int $userId): ?array
{
$userModel = new UserModel();
$patientModel = new PatientModel();
$user = $userModel->find($userId);
$patient = $patientModel->findByUserId($userId);
if (! $user || ! $patient) {
return null;
}
return [
'patient' => $patient,
'user' => $user,
'first_name' => $user['first_name'] ?? '',
'last_name' => $user['last_name'] ?? '',
];
}
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();
$doctorModel = new DoctorModel();
$specializationModel = new SpecializationModel();
$doctorSpecializationModel = new DoctorSpecializationModel();
$user = $userModel->find($userId);
$doctor = $doctorModel->findByUserId($userId);
if (! $user || ! $doctor) {
return null;
}
$selectedSpecializations = $doctorSpecializationModel->getNamesForDoctor((int) $doctor['id']);
if ($selectedSpecializations === []) {
$selectedSpecializations = $this->parseSpecializations($doctor['specialization'] ?? []);
}
return array_merge($this->parseExperienceString($doctor['experience'] ?? null), [
'doctor' => $doctor,
'user' => $user,
'first_name' => $user['first_name'] ?? '',
'last_name' => $user['last_name'] ?? '',
'specializationOptions' => $specializationModel->getOptionNames(),
'selectedSpecializations' => $selectedSpecializations,
]);
}
public function dashboard()
{
if ($r = $this->requireRole('admin')) {
@ -18,31 +165,30 @@ class Admin extends BaseController
$doctorModel = new DoctorModel();
$patientModel = new PatientModel();
$appointmentModel = new AppointmentModel();
$userModel = new UserModel();
$adminId = (int) session()->get('id');
$adminUser = $userModel->find($adminId);
$data['totalDoctors'] = $doctorModel->countAll();
$data['totalPatients'] = $patientModel->countAll();
$data['totalAppointments'] = $appointmentModel->countAll();
$data['activeToday'] = $appointmentModel->countForDate(date('Y-m-d'));
$data['recentActivity'] = $appointmentModel->getRecentActivity(6);
$data['latestDoctors'] = $doctorModel->getLatestDoctors(5);
$data['latestPatients'] = $patientModel->getLatestPatients(5);
$data['adminName'] = $this->formatFullName($adminUser['first_name'] ?? '', $adminUser['last_name'] ?? '', 'Administrator');
$data['adminEmail'] = $adminUser['email'] ?? '';
return view('admin/dashboard', $data);
}
public function doctors()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$db = \Config\Database::connect();
$query = $db->query("
SELECT users.id, users.name, users.email, doctors.specialization
FROM users
JOIN doctors ON doctors.user_id = users.id
WHERE users.role = 'doctor'
");
$data['doctors'] = $query->getResult();
return view('admin/doctors', $data);
return view('admin/doctors');
}
public function deleteDoctor($id)
@ -56,13 +202,15 @@ class Admin extends BaseController
return redirect()->to(site_url('admin/doctors'));
}
$userModel = new UserModel();
$doctorModel = new DoctorModel();
$db = \Config\Database::connect();
$userModel = new UserModel();
$doctorModel = new DoctorModel();
$appointmentModel = new AppointmentModel();
$doctorSpecializationModel = new DoctorSpecializationModel();
$doctor = $doctorModel->where('user_id', $id)->first();
$doctor = $doctorModel->findByUserId($id);
if ($doctor) {
$db->table('appointments')->where('doctor_id', $doctor['id'])->delete();
$appointmentModel->deleteByDoctorId((int) $doctor['id']);
$doctorSpecializationModel->deleteByDoctorId((int) $doctor['id']);
$doctorModel->delete($doctor['id']);
}
@ -77,18 +225,7 @@ class Admin extends BaseController
return $r;
}
$db = \Config\Database::connect();
$query = $db->query("
SELECT users.id, users.name, users.email, patients.phone
FROM users
JOIN patients ON patients.user_id = users.id
WHERE users.role = 'patient'
");
$data['patients'] = $query->getResult();
return view('admin/patients', $data);
return view('admin/patients');
}
public function deletePatient($id)
@ -102,13 +239,13 @@ class Admin extends BaseController
return redirect()->to(site_url('admin/patients'));
}
$userModel = new UserModel();
$patientModel = new PatientModel();
$db = \Config\Database::connect();
$userModel = new UserModel();
$patientModel = new PatientModel();
$appointmentModel = new AppointmentModel();
$patient = $patientModel->where('user_id', $id)->first();
$patient = $patientModel->findByUserId($id);
if ($patient) {
$db->table('appointments')->where('patient_id', $patient['id'])->delete();
$appointmentModel->deleteByPatientId((int) $patient['id']);
$patientModel->delete($patient['id']);
}
@ -123,18 +260,8 @@ class Admin extends BaseController
return $r;
}
$db = \Config\Database::connect();
$query = $db->query("
SELECT a.*, u1.name as patient_name, u2.name as doctor_name
FROM appointments a
JOIN patients p ON p.id = a.patient_id
JOIN users u1 ON u1.id = p.user_id
JOIN doctors d ON d.id = a.doctor_id
JOIN users u2 ON u2.id = d.user_id
");
$data['appointments'] = $query->getResult();
$appointmentModel = new AppointmentModel();
$data['appointments'] = $appointmentModel->getAdminAppointments();
return view('admin/appointments', $data);
}
@ -145,7 +272,63 @@ class Admin extends BaseController
return $r;
}
return view('admin/add_doctor');
$specializationModel = new SpecializationModel();
return view('admin/add_doctor', [
'specializationOptions' => $specializationModel->getOptionNames(),
'isEdit' => false,
]);
}
public function addPatient()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
return view('admin/add_patient', [
'isEdit' => false,
]);
}
public function editDoctor($encryptedId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = decrypt_id($encryptedId);
if (! ctype_digit((string) $id)) {
return redirect()->to(site_url('admin/doctors'))->with('error', 'Invalid doctor link.');
}
$doctorData = $this->getDoctorFormData((int) $id);
if (! $doctorData) {
return redirect()->to(site_url('admin/doctors'))->with('error', 'Doctor not found.');
}
return view('admin/add_doctor', array_merge($doctorData, [
'isEdit' => true,
]));
}
public function editPatient($encryptedId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = decrypt_id($encryptedId);
if (! ctype_digit((string) $id)) {
return redirect()->to(site_url('admin/patients'))->with('error', 'Invalid patient link.');
}
$patientData = $this->getPatientFormData((int) $id);
if (! $patientData) {
return redirect()->to(site_url('admin/patients'))->with('error', 'Patient not found.');
}
return view('admin/add_patient', array_merge($patientData, [
'isEdit' => true,
]));
}
public function storeDoctor()
@ -154,112 +337,161 @@ class Admin extends BaseController
return $r;
}
$userModel = new UserModel();
$doctorModel = new DoctorModel();
$db = \Config\Database::connect();
$validation = \Config\Services::validation();
$entries = $this->request->getPost('doctors');
if (! is_array($entries) || $entries === []) {
$entries = [[
'name' => $this->request->getPost('name'),
'email' => $this->request->getPost('email'),
'password' => $this->request->getPost('password'),
'specialization' => $this->request->getPost('specialization'),
'experience' => $this->request->getPost('experience'),
'fees' => $this->request->getPost('fees'),
'available_from' => $this->request->getPost('available_from'),
'available_to' => $this->request->getPost('available_to'),
]];
}
$rules = [
'name' => 'required|min_length[3]|max_length[100]',
'email' => 'required|valid_email',
'password' => 'required|min_length[8]',
'specialization' => 'required|min_length[2]|max_length[191]',
'experience' => 'permit_empty|max_length[100]',
'fees' => 'permit_empty|decimal',
'available_from' => 'permit_empty|valid_date[H:i]',
'available_to' => 'permit_empty|valid_date[H:i]',
'first_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]',
'experience_years' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[60]',
'experience_months' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[11]',
'fees' => 'permit_empty|decimal',
];
$emailsSeen = [];
$cleanRows = [];
$error = null;
foreach ($entries as $i => $row) {
$row = [
'name' => trim((string) ($row['name'] ?? '')),
'email' => trim((string) ($row['email'] ?? '')),
'password' => (string) ($row['password'] ?? ''),
'specialization' => trim((string) ($row['specialization'] ?? '')),
'experience' => trim((string) ($row['experience'] ?? '')),
'fees' => trim((string) ($row['fees'] ?? '')),
'available_from' => trim((string) ($row['available_from'] ?? '')),
'available_to' => trim((string) ($row['available_to'] ?? '')),
];
$rowNumber = $i + 1;
if (! $validation->setRules($rules)->run($row)) {
$rowErrors = $validation->getErrors();
$error = 'Row ' . $rowNumber . ': ' . implode(', ', array_values($rowErrors));
break;
}
$emailKey = strtolower($row['email']);
if (isset($emailsSeen[$emailKey])) {
$error = 'Row ' . $rowNumber . ': Duplicate email in submitted rows.';
break;
}
$emailsSeen[$emailKey] = true;
if ($userModel->where('email', $row['email'])->first()) {
$error = 'Row ' . $rowNumber . ': Email already exists.';
break;
}
$cleanRows[] = $row;
if (! $this->validate($rules)) {
return redirect()->back()->withInput();
}
if ($error !== null || $cleanRows === []) {
return redirect()->back()->withInput()->with('error', $error ?? 'Please provide at least one doctor row.');
$userModel = new UserModel();
$doctorModel = new DoctorModel();
$specializationModel = new SpecializationModel();
$doctorSpecializationModel = new DoctorSpecializationModel();
$db = \Config\Database::connect();
$firstName = trim((string) $this->request->getPost('first_name'));
$lastName = trim((string) $this->request->getPost('last_name'));
$specializationInput = $this->request->getPost('specialization');
$specializations = $this->parseSpecializations($specializationInput);
if ($specializations === []) {
return redirect()->back()->withInput()->with('error', 'Please select at least one specialization.');
}
$specializationValue = implode(', ', $specializations);
if (strlen($specializationValue) > 191) {
return redirect()->back()->withInput()->with('error', 'Selected specializations are too long. Please reduce them.');
}
$yearsRaw = (string) $this->request->getPost('experience_years');
$monthsRaw = (string) $this->request->getPost('experience_months');
$years = $yearsRaw === '' ? null : (int) $yearsRaw;
$months = $monthsRaw === '' ? null : (int) $monthsRaw;
$experience = $this->buildExperienceString($years, $months);
$generatedPassword = $this->generateAccountPassword();
$db->transStart();
foreach ($cleanRows as $row) {
$userData = [
'name' => $row['name'],
'email' => $row['email'],
'password' => password_hash($row['password'], PASSWORD_DEFAULT),
'role' => 'doctor',
'status' => 'active',
];
$userData = [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => trim((string) $this->request->getPost('email')),
'password' => password_hash($generatedPassword, PASSWORD_DEFAULT),
'role' => 'doctor',
'status' => 'active',
];
if (! $userModel->skipValidation(true)->insert($userData)) {
$db->transRollback();
if (! $userModel->skipValidation(true)->insert($userData)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create user for ' . $row['email'] . '.');
}
return redirect()->back()->withInput()->with('error', 'Could not create doctor login account.');
}
$userId = (int) $userModel->getInsertID();
$userId = (int) $userModel->getInsertID();
$doctorRow = [
'user_id' => $userId,
'specialization' => $row['specialization'],
'experience' => $row['experience'] !== '' ? $row['experience'] : null,
'fees' => $row['fees'] !== '' ? $row['fees'] : null,
'available_from' => $row['available_from'] !== '' ? $row['available_from'] : null,
'available_to' => $row['available_to'] !== '' ? $row['available_to'] : null,
];
$doctorRow = [
'user_id' => $userId,
'specialization' => $specializationValue,
'experience' => $experience,
'fees' => $this->request->getPost('fees') !== '' && $this->request->getPost('fees') !== null
? $this->request->getPost('fees')
: null,
];
if (! $doctorModel->skipValidation(true)->insert($doctorRow)) {
$db->transRollback();
if (! $doctorModel->skipValidation(true)->insert($doctorRow)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create doctor profile for ' . $row['email'] . '.');
}
return redirect()->back()->withInput()->with('error', 'Could not create doctor profile.');
}
$doctorId = (int) $doctorModel->getInsertID();
$specializationMap = $specializationModel->ensureNamesExist($specializations);
$specializationIds = array_values($specializationMap);
$doctorSpecializationModel->syncDoctorSpecializations($doctorId, $specializationIds, (int) session()->get('id'));
$db->transComplete();
if (! $db->transStatus()) {
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);
}
public function storePatient()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$rules = [
'first_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]',
'phone' => 'required|min_length[10]|max_length[15]',
'dob' => 'permit_empty|valid_date[Y-m-d]',
'gender' => 'permit_empty|in_list[male,female,other]',
];
if (! $this->validate($rules)) {
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'));
$generatedPassword = $this->generateAccountPassword();
$dobRaw = trim((string) $this->request->getPost('dob'));
$db->transStart();
$userData = [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => trim((string) $this->request->getPost('email')),
'password' => password_hash($generatedPassword, PASSWORD_DEFAULT),
'role' => 'patient',
'status' => 'active',
];
if (! $userModel->skipValidation(true)->insert($userData)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create patient login account.');
}
$userId = (int) $userModel->getInsertID();
$patientRow = [
'user_id' => $userId,
'dob' => $dobRaw !== '' ? $dobRaw : null,
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
'phone' => trim((string) $this->request->getPost('phone')),
];
if (! $patientModel->skipValidation(true)->insert($patientRow)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create patient profile.');
}
$db->transComplete();
@ -268,8 +500,232 @@ class Admin extends BaseController
return redirect()->back()->withInput()->with('error', 'Transaction failed.');
}
$created = count($cleanRows);
$logModel = new ActivityLogModel();
$logModel->log('create_patient', "Admin created patient {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/doctors'))->with('success', $created . ' doctor account(s) created.');
return redirect()->to(site_url('admin/patients'))->with('success', 'Patient account created. Generated password: ' . $generatedPassword);
}
public function updateDoctor($encryptedId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = decrypt_id($encryptedId);
if (! ctype_digit((string) $id)) {
return redirect()->to(site_url('admin/doctors'))->with('error', 'Invalid doctor link.');
}
$userId = (int) $id;
$doctorData = $this->getDoctorFormData($userId);
if (! $doctorData) {
return redirect()->to(site_url('admin/doctors'))->with('error', 'Doctor not found.');
}
$rules = [
'first_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 . ']',
'experience_months' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[11]',
'experience_years' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[60]',
'fees' => 'permit_empty|decimal',
];
if (! $this->validate($rules)) {
return redirect()->back()->withInput();
}
$userModel = new UserModel();
$doctorModel = new DoctorModel();
$specializationModel = new SpecializationModel();
$doctorSpecializationModel = new DoctorSpecializationModel();
$db = \Config\Database::connect();
$firstName = trim((string) $this->request->getPost('first_name'));
$lastName = trim((string) $this->request->getPost('last_name'));
$specializationInput = $this->request->getPost('specialization');
$specializations = $this->parseSpecializations($specializationInput);
if ($specializations === []) {
return redirect()->back()->withInput()->with('error', 'Please select at least one specialization.');
}
$specializationValue = implode(', ', $specializations);
if (strlen($specializationValue) > 191) {
return redirect()->back()->withInput()->with('error', 'Selected specializations are too long. Please reduce them.');
}
$yearsRaw = (string) $this->request->getPost('experience_years');
$monthsRaw = (string) $this->request->getPost('experience_months');
$years = $yearsRaw === '' ? null : (int) $yearsRaw;
$months = $monthsRaw === '' ? null : (int) $monthsRaw;
$experience = $this->buildExperienceString($years, $months);
$db->transStart();
$userModel->update($userId, [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => trim((string) $this->request->getPost('email')),
]);
$doctorModel->update($doctorData['doctor']['id'], [
'specialization' => $specializationValue,
'experience' => $experience,
'fees' => $this->request->getPost('fees') !== '' && $this->request->getPost('fees') !== null
? $this->request->getPost('fees')
: null,
]);
$specializationMap = $specializationModel->ensureNamesExist($specializations);
$doctorSpecializationModel->syncDoctorSpecializations((int) $doctorData['doctor']['id'], array_values($specializationMap), (int) session()->get('id'));
$db->transComplete();
if (! $db->transStatus()) {
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.');
}
public function updatePatient($encryptedId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = decrypt_id($encryptedId);
if (! ctype_digit((string) $id)) {
return redirect()->to(site_url('admin/patients'))->with('error', 'Invalid patient link.');
}
$userId = (int) $id;
$patientData = $this->getPatientFormData($userId);
if (! $patientData) {
return redirect()->to(site_url('admin/patients'))->with('error', 'Patient not found.');
}
$rules = [
'first_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 . ']',
'phone' => 'required|min_length[10]|max_length[15]',
'dob' => 'permit_empty|valid_date[Y-m-d]',
'gender' => 'permit_empty|in_list[male,female,other]',
];
if (! $this->validate($rules)) {
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'));
$dobRaw = trim((string) $this->request->getPost('dob'));
$db->transStart();
$userModel->update($userId, [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => trim((string) $this->request->getPost('email')),
]);
$patientModel->update($patientData['patient']['id'], [
'dob' => $dobRaw !== '' ? $dobRaw : null,
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
'phone' => trim((string) $this->request->getPost('phone')),
]);
$db->transComplete();
if (! $db->transStatus()) {
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()
{
$email = (string) $this->request->getPost('email');
$excludeId = (int) $this->request->getPost('exclude_id');
$userModel = new UserModel();
return $this->response->setJSON([
'exists' => $userModel->emailExistsExcept($email, $excludeId > 0 ? $excludeId : null)
]);
}
public function getDoctors()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
helper('encryption');
$doctorModel = new DoctorModel();
$doctors = $doctorModel->getAdminDoctorList('doctor_id', 'asc');
$payload = [];
foreach ($doctors as $doc) {
$payload[] = [
'user_id' => (int) ($doc->user_id ?? 0),
'formatted_user_id' => $doc->formatted_user_id ?? null,
'name' => $doc->name ?? null,
'email' => $doc->email ?? null,
'specialization' => $doc->specialization ?? null,
'experience' => $doc->experience ?? null,
'fees' => $doc->fees ?? null,
'status' => $doc->status ?? null,
'edit_token' => encrypt_id($doc->user_id ?? 0),
];
}
return $this->response->setJSON([
'data' => $payload,
]);
}
public function getPatients()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
helper('encryption');
$patientModel = new PatientModel();
$patients = $patientModel->getAdminPatientList('patient_id', 'asc');
$payload = [];
foreach ($patients as $patient) {
$payload[] = [
'user_id' => (int) ($patient->user_id ?? 0),
'formatted_user_id' => $patient->formatted_user_id ?? null,
'name' => $patient->name ?? null,
'email' => $patient->email ?? null,
'phone' => $patient->phone ?? null,
'status' => $patient->status ?? null,
'edit_token' => encrypt_id($patient->user_id ?? 0),
];
}
return $this->response->setJSON([
'data' => $payload,
]);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Controllers;
use App\Models\ActivityLogModel;
use App\Models\UserModel;
use App\Models\PatientModel;
@ -20,10 +21,11 @@ class Auth extends BaseController
public function registerProcess()
{
$rules = [
'name' => 'required|min_length[3]|max_length[100]|alpha_numeric_punct',
'email' => 'required|valid_email|is_unique[users.email]',
'phone' => 'required|min_length[10]|max_length[10]',
'password' => 'required|min_length[8]',
'first_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]',
'phone' => 'required|regex_match[/^[6-9]\d{9}$/]',
'password' => 'required|min_length[8]|regex_match[/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z\d]).+$/]',
];
if (! $this->validate($rules)) {
@ -31,13 +33,16 @@ class Auth extends BaseController
}
$userModel = new UserModel();
$firstName = trim((string) $this->request->getPost('first_name'));
$lastName = trim((string) $this->request->getPost('last_name'));
$data = [
'name' => $this->request->getPost('name'),
'email' => $this->request->getPost('email'),
'password' => password_hash((string) $this->request->getPost('password'), PASSWORD_DEFAULT),
'role' => 'patient',
'status' => 'active',
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $this->request->getPost('email'),
'password' => password_hash((string) $this->request->getPost('password'), PASSWORD_DEFAULT),
'role' => 'patient',
'status' => 'active',
];
if (! $userModel->skipValidation(true)->insert($data)) {
@ -49,9 +54,12 @@ class Auth extends BaseController
$patientModel = new PatientModel();
$patientModel->insert([
'user_id' => $user_id,
'phone' => $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.');
}
@ -87,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'));
}
@ -103,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')
@ -166,7 +183,7 @@ class Auth extends BaseController
{
$rules = [
'token' => 'required',
'password' => 'required|min_length[8]',
'password' => 'required|min_length[8]|regex_match[/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z\d]).+$/]',
];
if (! $this->validate($rules)) {

View File

@ -3,12 +3,54 @@
namespace App\Controllers;
use App\Models\AppointmentModel;
use App\Models\ActivityLogModel;
use App\Models\DoctorModel;
use App\Models\DoctorSpecializationModel;
use App\Models\SpecializationModel;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface;
class Doctor extends BaseController
{
private function parseSpecializations($specializationInput): array
{
$specializations = [];
if (is_array($specializationInput)) {
foreach ($specializationInput as $item) {
$item = trim((string) $item);
if ($item !== '' && ! in_array($item, $specializations, true)) {
$specializations[] = $item;
}
}
} else {
$parts = explode(',', (string) $specializationInput);
foreach ($parts as $item) {
$item = trim($item);
if ($item !== '' && ! in_array($item, $specializations, true)) {
$specializations[] = $item;
}
}
}
return $specializations;
}
private function getDoctorSpecializationNames(int $doctorId): array
{
$db = \Config\Database::connect();
$rows = $db->table('doctor_specializations ds')
->select('s.name')
->join('specializations s', 's.id = ds.specialization_id')
->where('ds.doctor_id', $doctorId)
->orderBy('s.name', 'ASC')
->get()
->getResultArray();
return array_map(static fn ($row) => $row['name'], $rows);
}
public function dashboard()
{
if ($r = $this->requireRole('doctor')) {
@ -27,7 +69,8 @@ class Doctor extends BaseController
$doctorId = (int) $doctor['id'];
$query = $db->query('
SELECT a.*, u.name as patient_name
SELECT a.*,
TRIM(CONCAT(COALESCE(u.first_name, \'\'), \' \', COALESCE(u.last_name, \'\'))) AS patient_name
FROM appointments a
JOIN patients p ON p.id = a.patient_id
JOIN users u ON u.id = p.user_id
@ -46,6 +89,7 @@ class Doctor extends BaseController
}
$doctorModel = new DoctorModel();
$specializationModel = new SpecializationModel();
$userId = (int) session()->get('id');
$doctor = $doctorModel->where('user_id', $userId)->first();
@ -55,35 +99,53 @@ class Doctor extends BaseController
if ($this->request->is('post')) {
$rules = [
'specialization' => 'required|min_length[2]|max_length[191]',
'specialization' => 'required',
'experience' => 'required|max_length[100]',
'fees' => 'permit_empty|decimal',
'available_from' => 'permit_empty',
'available_to' => 'permit_empty',
];
if (! $this->validate($rules)) {
return redirect()->back()->withInput();
}
$specializations = $this->parseSpecializations($this->request->getPost('specialization'));
if ($specializations === []) {
return redirect()->back()->withInput()->with('error', 'Please select at least one specialization.');
}
$update = [
'specialization' => $this->request->getPost('specialization'),
'specialization' => implode(', ', $specializations),
'experience' => $this->request->getPost('experience') ?: null,
'fees' => $this->request->getPost('fees') !== '' && $this->request->getPost('fees') !== null
? $this->request->getPost('fees')
: null,
'available_from' => $this->request->getPost('available_from') ?: null,
'available_to' => $this->request->getPost('available_to') ?: null,
];
if (! $doctorModel->update($doctor['id'], $update)) {
return redirect()->back()->withInput()->with('error', 'Could not update profile.');
}
$specializationMap = $specializationModel->ensureNamesExist($specializations);
$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.');
}
return view('doctor/profile', ['doctor' => $doctor]);
$selectedSpecializations = $this->getDoctorSpecializationNames((int) $doctor['id']);
if ($selectedSpecializations === [] && ! empty($doctor['specialization'])) {
$selectedSpecializations = $this->parseSpecializations($doctor['specialization']);
}
return view('doctor/profile', [
'doctor' => $doctor,
'specializationOptions' => $specializationModel->getOptionNames(),
'selectedSpecializations' => $selectedSpecializations,
]);
}
public function accept($id): ResponseInterface
@ -135,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.');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Controllers;
use App\Models\AppointmentModel;
use App\Models\ActivityLogModel;
use App\Models\PatientModel;
class Patient extends BaseController
@ -23,7 +24,9 @@ class Patient extends BaseController
$db = \Config\Database::connect();
$query = $db->query("
SELECT doctors.id AS doctor_id, users.name, doctors.specialization
SELECT doctors.id AS doctor_id,
TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, ''))) AS name,
doctors.specialization
FROM users
JOIN doctors ON doctors.user_id = users.id
WHERE users.role = 'doctor'
@ -38,7 +41,8 @@ class Patient extends BaseController
if ($patient) {
$data['myAppointments'] = $db->query('
SELECT a.id, a.appointment_date, a.appointment_time, a.status,
u.name AS doctor_name, doctors.specialization
TRIM(CONCAT(COALESCE(u.first_name, \'\'), \' \', COALESCE(u.last_name, \'\'))) AS doctor_name,
doctors.specialization
FROM appointments a
JOIN doctors ON doctors.id = a.doctor_id
JOIN users u ON u.id = doctors.user_id
@ -100,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.');
}
}

View File

@ -13,7 +13,8 @@ class InitAppointmentSchema extends Migration
{
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 100],
'first_name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
'last_name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
'email' => ['type' => 'VARCHAR', 'constraint' => 191],
'password' => ['type' => 'VARCHAR', 'constraint' => 255],
'role' => ['type' => 'VARCHAR', 'constraint' => 20],
@ -39,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],
]);

View File

@ -0,0 +1,84 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateDoctorSpecializations extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 100],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('name');
$this->forge->createTable('specializations', true);
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'doctor_id' => ['type' => 'INT', 'unsigned' => true],
'specialization_id' => ['type' => 'INT', 'unsigned' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('doctor_id');
$this->forge->addKey('specialization_id');
$this->forge->addUniqueKey(['doctor_id', 'specialization_id']);
$this->forge->createTable('doctor_specializations', true);
$db = \Config\Database::connect();
if (! $db->tableExists('doctors')) {
return;
}
$doctors = $db->table('doctors')
->select('id, specialization')
->where('specialization IS NOT NULL')
->where('specialization !=', '')
->get()
->getResultArray();
foreach ($doctors as $doctor) {
$rawNames = explode(',', (string) $doctor['specialization']);
$names = [];
foreach ($rawNames as $name) {
$name = trim($name);
if ($name !== '' && ! in_array($name, $names, true)) {
$names[] = $name;
}
}
foreach ($names as $name) {
$existing = $db->table('specializations')->where('name', $name)->get()->getRowArray();
if ($existing) {
$specializationId = (int) $existing['id'];
} else {
$db->table('specializations')->insert(['name' => $name]);
$specializationId = (int) $db->insertID();
}
$pivotExists = $db->table('doctor_specializations')
->where('doctor_id', (int) $doctor['id'])
->where('specialization_id', $specializationId)
->countAllResults() > 0;
if (! $pivotExists) {
$db->table('doctor_specializations')->insert([
'doctor_id' => (int) $doctor['id'],
'specialization_id' => $specializationId,
]);
}
}
}
}
public function down(): void
{
$this->forge->dropTable('doctor_specializations', true);
$this->forge->dropTable('specializations', true);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Database\Migrations;
use App\Models\SpecializationModel;
use CodeIgniter\Database\Migration;
class SeedDefaultSpecializations extends Migration
{
public function up(): void
{
if (! $this->db->tableExists('specializations')) {
return;
}
$model = new SpecializationModel();
$model->ensureNamesExist(SpecializationModel::defaultNames());
}
public function down(): void
{
if (! $this->db->tableExists('specializations')) {
return;
}
$this->db->table('specializations')
->whereIn('name', SpecializationModel::defaultNames())
->delete();
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddAuditFieldsToDoctorSpecializations extends Migration
{
public function up(): void
{
if (! $this->db->tableExists('doctor_specializations')) {
return;
}
$fields = [];
if (! $this->db->fieldExists('status', 'doctor_specializations')) {
$fields['status'] = [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
'after' => 'specialization_id',
];
}
if (! $this->db->fieldExists('created_at', 'doctor_specializations')) {
$fields['created_at'] = [
'type' => 'DATETIME',
'null' => true,
'after' => 'status',
];
}
if (! $this->db->fieldExists('updated_at', 'doctor_specializations')) {
$fields['updated_at'] = [
'type' => 'DATETIME',
'null' => true,
'after' => 'created_at',
];
}
if (! $this->db->fieldExists('created_by', 'doctor_specializations')) {
$fields['created_by'] = [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'after' => 'updated_at',
];
}
if (! $this->db->fieldExists('updated_by', 'doctor_specializations')) {
$fields['updated_by'] = [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'after' => 'created_by',
];
}
if ($fields !== []) {
$this->forge->addColumn('doctor_specializations', $fields);
}
$now = date('Y-m-d H:i:s');
$this->db->table('doctor_specializations')->set([
'status' => 1,
'created_at' => $now,
'updated_at' => $now,
])->update();
}
public function down(): void
{
if (! $this->db->tableExists('doctor_specializations')) {
return;
}
foreach (['updated_by', 'created_by', 'updated_at', 'created_at', 'status'] as $field) {
if ($this->db->fieldExists($field, 'doctor_specializations')) {
$this->forge->dropColumn('doctor_specializations', $field);
}
}
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class UseNumericStatusForDoctorSpecializations extends Migration
{
public function up(): void
{
if (! $this->db->tableExists('doctor_specializations') || ! $this->db->fieldExists('status', 'doctor_specializations')) {
return;
}
$this->db->query("UPDATE `doctor_specializations` SET `status` = '1' WHERE `status` IS NULL OR `status` = '' OR LOWER(`status`) = 'active'");
$this->db->query("UPDATE `doctor_specializations` SET `status` = '0' WHERE LOWER(`status`) = 'inactive'");
$this->forge->modifyColumn('doctor_specializations', [
'status' => [
'name' => 'status',
'type' => 'TINYINT',
'constraint' => 1,
'null' => false,
'default' => 1,
],
]);
}
public function down(): void
{
if (! $this->db->tableExists('doctor_specializations') || ! $this->db->fieldExists('status', 'doctor_specializations')) {
return;
}
$this->db->query("UPDATE `doctor_specializations` SET `status` = 'active' WHERE `status` = 1");
$this->db->query("UPDATE `doctor_specializations` SET `status` = 'inactive' WHERE `status` = 0");
$this->forge->modifyColumn('doctor_specializations', [
'status' => [
'name' => 'status',
'type' => 'VARCHAR',
'constraint' => 20,
'null' => false,
'default' => 'active',
],
]);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddFirstNameLastNameToUsers extends Migration
{
public function up(): void
{
if (! $this->db->fieldExists('first_name', 'users')) {
$this->forge->addColumn('users', [
'first_name' => [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
],
]);
}
if (! $this->db->fieldExists('last_name', 'users')) {
$this->forge->addColumn('users', [
'last_name' => [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
],
]);
}
if ($this->db->fieldExists('name', 'users')) {
$this->db->query("
UPDATE users
SET
first_name = CASE
WHEN LOCATE(' ', name) > 0 THEN SUBSTRING_INDEX(name, ' ', 1)
ELSE name
END,
last_name = CASE
WHEN LOCATE(' ', name) > 0 THEN SUBSTRING_INDEX(name, ' ', -1)
ELSE NULL
END
WHERE first_name IS NULL AND name IS NOT NULL
");
}
}
public function down(): void
{
if ($this->db->fieldExists('first_name', 'users')) {
$this->forge->dropColumn('users', 'first_name');
}
if ($this->db->fieldExists('last_name', 'users')) {
$this->forge->dropColumn('users', 'last_name');
}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddFormattedUserIdToUsers extends Migration
{
public function up()
{
$this->forge->addColumn('users', [
'formatted_user_id' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => false,
'after' => 'id',
],
]);
}
public function down()
{
$this->forge->dropColumn('users', 'formatted_user_id');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class BackfillFormattedUserIdsByRole extends Migration
{
public function up()
{
$this->db->query("
UPDATE users
SET formatted_user_id = CONCAT('PAT', LPAD(id, 7, '0'))
WHERE role = 'patient'
");
$this->db->query("
UPDATE users
SET formatted_user_id = CONCAT('PHY', LPAD(id, 7, '0'))
WHERE role = 'doctor'
");
}
public function down()
{
$this->db->query("
UPDATE users
SET formatted_user_id = CONCAT('PHY', LPAD(id, 7, '0'))
WHERE role = 'patient'
");
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class DropNameFromUsers extends Migration
{
public function up(): void
{
if ($this->db->fieldExists('name', 'users')) {
$this->forge->dropColumn('users', 'name');
}
}
public function down(): void
{
if ($this->db->fieldExists('name', 'users')) {
return;
}
$this->forge->addColumn('users', [
'name' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
'after' => 'id',
],
]);
if ($this->db->fieldExists('first_name', 'users') && $this->db->fieldExists('last_name', 'users')) {
$this->db->query("
UPDATE users
SET name = TRIM(CONCAT(COALESCE(first_name, ''), ' ', COALESCE(last_name, '')))
WHERE name IS NULL
");
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
<?php
function encrypt_id($id)
{
$key = 'SN28062003';
$encrypted = openssl_encrypt((string) $id, 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
if ($encrypted === false) {
return '';
}
return rtrim(strtr(base64_encode($encrypted), '+/', '-_'), '=');
}
function decrypt_id($encrypted)
{
$key = 'SN28062003';
$decoded = base64_decode(strtr((string) $encrypted, '-_', '+/') . str_repeat('=', (4 - strlen((string) $encrypted) % 4) % 4), true);
if ($decoded === false) {
return null;
}
$decrypted = openssl_decrypt($decoded, 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
return $decrypted === false ? null : $decrypted;
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class ActivityLogModel extends Model
{
protected $table = 'activity_logs';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $protectFields = true;
protected $allowedFields = [
'actor_user_id',
'actor_role',
'action',
'description',
'target_type',
'target_id',
'ip_address',
'user_agent',
'created_at',
];
public function log(string $action, string $description, ?string $targetType = null, ?int $targetId = null): void
{
$request = service('request');
$this->insert([
'actor_user_id' => session()->get('id') ? (int) session()->get('id') : null,
'actor_role' => session()->get('role') ?: null,
'action' => $action,
'description' => $description,
'target_type' => $targetType,
'target_id' => $targetId,
'ip_address' => method_exists($request, 'getIPAddress') ? $request->getIPAddress() : null,
'user_agent' => method_exists($request, 'getUserAgent') ? $request->getUserAgent()->getAgentString() : null,
'created_at' => date('Y-m-d H:i:s'),
]);
}
public function getFiltered(array $filters = []): array
{
$builder = $this->db->table('activity_logs al')
->select("al.*, CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, '')) AS actor_name, u.email AS actor_email")
->join('users u', 'u.id = al.actor_user_id', 'left');
if (! empty($filters['action'])) {
$builder->like('al.action', $filters['action']);
}
if (! empty($filters['role'])) {
$builder->where('al.actor_role', $filters['role']);
}
if (! empty($filters['date_from'])) {
$builder->where('DATE(al.created_at) >=', $filters['date_from']);
}
if (! empty($filters['date_to'])) {
$builder->where('DATE(al.created_at) <=', $filters['date_to']);
}
return $builder
->orderBy('al.created_at', 'DESC')
->get()
->getResultArray();
}
public function getRecent(int $limit = 8): array
{
return $this->db->table('activity_logs')
->orderBy('created_at', 'DESC')
->limit($limit)
->get()
->getResultArray();
}
}

View File

@ -67,6 +67,44 @@ class AppointmentModel extends Model
return $eventData;
}
public function countForDate(string $date): int
{
return $this->where('appointment_date', $date)->countAllResults();
}
public function getRecentActivity(int $limit = 6): array
{
return $this->select("appointments.status, appointments.appointment_date, appointments.appointment_time, TRIM(CONCAT(COALESCE(u1.first_name, ''), ' ', COALESCE(u1.last_name, ''))) AS patient_name, TRIM(CONCAT(COALESCE(u2.first_name, ''), ' ', COALESCE(u2.last_name, ''))) AS doctor_name")
->join('patients p', 'p.id = appointments.patient_id')
->join('users u1', 'u1.id = p.user_id')
->join('doctors d', 'd.id = appointments.doctor_id')
->join('users u2', 'u2.id = d.user_id')
->orderBy('appointments.id', 'DESC')
->findAll($limit);
}
public function getAdminAppointments(): array
{
return $this->asObject()
->select("appointments.*, TRIM(CONCAT(COALESCE(u1.first_name, ''), ' ', COALESCE(u1.last_name, ''))) AS patient_name, TRIM(CONCAT(COALESCE(u2.first_name, ''), ' ', COALESCE(u2.last_name, ''))) AS doctor_name")
->join('patients p', 'p.id = appointments.patient_id')
->join('users u1', 'u1.id = p.user_id')
->join('doctors d', 'd.id = appointments.doctor_id')
->join('users u2', 'u2.id = d.user_id')
->findAll();
}
public function deleteByDoctorId(int $doctorId): void
{
$this->where('doctor_id', $doctorId)->delete();
}
public function deleteByPatientId(int $patientId): void
{
$this->where('patient_id', $patientId)->delete();
}
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];

View File

@ -12,7 +12,7 @@ class DoctorModel extends Model
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['user_id','specialization','experience','fees','available_from','available_to'];
protected $allowedFields = ['user_id','specialization','experience','fees'];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
@ -43,4 +43,44 @@ class DoctorModel extends Model
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
public function findByUserId(int $userId): ?array
{
$row = $this->where('user_id', $userId)->first();
return $row ?: null;
}
public function getAdminDoctorList(string $sortBy = 'doctor_id', string $sortDir = 'asc'): array
{
$fullNameSql = "TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, '')))";
$sortColumns = [
'doctor_id' => 'users.id',
'name' => $fullNameSql,
'email' => 'users.email',
'specialization' => 'doctors.specialization',
'experience' => 'doctors.experience',
'fees' => 'doctors.fees',
'status' => 'users.status',
];
$sortColumn = $sortColumns[$sortBy] ?? 'users.id';
$sortDir = strtolower($sortDir) === 'desc' ? 'DESC' : 'ASC';
return $this->asObject()
->select("users.id as user_id, COALESCE(NULLIF(users.formatted_user_id, ''), CONCAT('PHY', LPAD(users.id, 7, '0'))) as formatted_user_id, {$fullNameSql} as name, users.email, users.first_name, users.last_name, users.status, doctors.id, doctors.specialization, doctors.experience, doctors.fees")
->join('users', 'users.id = doctors.user_id')
->where('users.role', 'doctor')
->orderBy($sortColumn, $sortDir)
->findAll();
}
public function getLatestDoctors(int $limit = 5): array
{
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);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class DoctorSpecializationModel extends Model
{
protected $table = 'doctor_specializations';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $protectFields = true;
protected $allowedFields = [
'doctor_id',
'specialization_id',
'status',
'created_by',
'updated_by',
];
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
public function getNamesForDoctor(int $doctorId): array
{
$rows = $this->select('specializations.name')
->join('specializations', 'specializations.id = doctor_specializations.specialization_id')
->where('doctor_specializations.doctor_id', $doctorId)
->findAll();
return array_values(array_filter(array_map(
static fn (array $row) => ! empty($row['name']) ? $row['name'] : null,
$rows
)));
}
public function deleteByDoctorId(int $doctorId): void
{
$this->where('doctor_id', $doctorId)->delete();
}
public function syncDoctorSpecializations(int $doctorId, array $specializationIds, ?int $actorId = null): void
{
$specializationIds = array_values(array_unique(array_map('intval', $specializationIds)));
$existingRows = $this->where('doctor_id', $doctorId)->findAll();
$existingBySpecId = [];
foreach ($existingRows as $row) {
$existingBySpecId[(int) $row['specialization_id']] = $row;
}
$existingIds = array_keys($existingBySpecId);
$toDelete = array_diff($existingIds, $specializationIds);
$toInsert = array_diff($specializationIds, $existingIds);
if ($toDelete !== []) {
$this->where('doctor_id', $doctorId)
->whereIn('specialization_id', $toDelete)
->delete();
}
foreach ($toInsert as $specializationId) {
$this->insert([
'doctor_id' => $doctorId,
'specialization_id' => $specializationId,
'status' => 1,
'created_by' => $actorId,
'updated_by' => $actorId,
]);
}
}
}

View File

@ -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;
@ -43,4 +43,42 @@ class PatientModel extends Model
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
public function findByUserId(int $userId): ?array
{
$row = $this->where('user_id', $userId)->first();
return $row ?: null;
}
public function getAdminPatientList(string $sortBy = 'patient_id', string $sortDir = 'asc'): array
{
$fullNameSql = "TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, '')))";
$sortColumns = [
'patient_id' => 'users.id',
'name' => $fullNameSql,
'email' => 'users.email',
'phone' => 'patients.phone',
'status' => 'users.status',
];
$sortColumn = $sortColumns[$sortBy] ?? 'users.id';
$sortDir = strtolower($sortDir) === 'desc' ? 'DESC' : 'ASC';
return $this->asObject()
->select("users.id as user_id, 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, {$fullNameSql} as name, users.email, users.first_name, users.last_name, users.status, patients.id, patients.phone")
->join('users', 'users.id = patients.user_id')
->where('users.role', 'patient')
->orderBy($sortColumn, $sortDir)
->findAll();
}
public function getLatestPatients(int $limit = 5): array
{
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);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class SpecializationModel extends Model
{
protected $table = 'specializations';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $protectFields = true;
protected $allowedFields = ['name', 'status', 'created_at'];
protected $useTimestamps = true;
protected $createdField = 'created_at';
public function getOptionNames(): array
{
$databaseNames = $this->select('name')->orderBy('name', 'ASC')->findColumn('name') ?: [];
natcasesort($databaseNames);
return array_values(array_unique($databaseNames));
}
public function ensureNamesExist(array $names): array
{
$names = array_values(array_unique(array_filter(array_map(
static fn ($name) => trim((string) $name),
$names
))));
if ($names === []) {
return [];
}
$existingRows = $this->whereIn('name', $names)->findAll();
$nameToId = [];
foreach ($existingRows as $row) {
$nameToId[$row['name']] = (int) $row['id'];
}
foreach ($names as $name) {
if (isset($nameToId[$name])) {
continue;
}
$this->insert(['name' => $name, 'status' => 1]);
$nameToId[$name] = (int) $this->getInsertID();
}
return $nameToId;
}
}

View File

@ -13,7 +13,9 @@ class UserModel extends Model
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [
'name',
'formatted_user_id',
'first_name',
'last_name',
'email',
'password',
'role',
@ -45,11 +47,61 @@ class UserModel extends Model
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $afterInsert = ['assignFormattedUserId'];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
public function emailExists(string $email): bool
{
return $this->where('email', trim($email))->first() !== null;
}
public function emailExistsExcept(string $email, ?int $excludeUserId = null): bool
{
$builder = $this->where('email', trim($email));
if ($excludeUserId !== null && $excludeUserId > 0) {
$builder->where('id !=', $excludeUserId);
}
return $builder->first() !== null;
}
protected function assignFormattedUserId(array $data): array
{
$insertId = (int) ($data['id'] ?? 0);
$role = strtolower((string) ($data['data']['role'] ?? ''));
if ($insertId < 1) {
return $data;
}
if ($role === '') {
$user = $this->find($insertId);
$role = strtolower((string) ($user['role'] ?? ''));
}
$this->builder()
->where('id', $insertId)
->where('(formatted_user_id IS NULL OR formatted_user_id = "")')
->update([
'formatted_user_id' => $this->formatUserId($insertId, $role),
]);
return $data;
}
public function formatUserId(int $userId, ?string $role = null): string
{
$prefix = match (strtolower((string) $role)) {
'patient' => 'PAT',
'doctor' => 'PHY',
};
return $prefix . str_pad((string) $userId, 7, '0', STR_PAD_LEFT);
}
}

View File

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

View File

@ -3,147 +3,259 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Add doctor</title>
<title>Add Doctor</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 href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<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') ?>">
</head>
<body class="app-body app-page--admin"5>
<?php
$oldDoctors = old('doctors');
if (! is_array($oldDoctors) || $oldDoctors === []) {
$oldDoctors = [[
'name' => '',
'email' => '',
'password' => '',
'specialization' => '',
'experience' => '',
'fees' => '',
'available_from' => '',
'available_to' => '',
]];
<body class="app-body overview-layout">
<?php $validationErrors = validation_errors(); ?>
<?php $specializationOptions = $specializationOptions ?? [];
$isEdit = $isEdit ?? false;
$doctor = $doctor ?? null;
$user = $user ?? null;
$oldSpecializations = old('specialization', $selectedSpecializations ?? []);
if (! is_array($oldSpecializations)) {
$oldSpecializations = $oldSpecializations ? [(string) $oldSpecializations] : [];
}
?>
<div class="container py-5" style="max-width: 980px;">
<h2 class="text-center mb-4 app-heading">Add Doctors</h2>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<form method="post" action="<?= base_url('admin/doctors/add') ?>" class="app-form card app-card-dashboard p-4" novalidate id="bulk-doctor-form">
<?= csrf_field() ?>
<div class="d-flex justify-content-between align-items-center mb-3">
<p class="mb-0 text-muted">Add one or many doctors, then submit once.</p>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3" id="add-doctor-row">+ Add another doctor</button>
<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 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>
<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"><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 id="doctor-rows">
<?php foreach ($oldDoctors as $index => $doctor): ?>
<div class="border rounded-4 p-3 mb-3 doctor-row" data-row>
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Doctor No: <span data-row-number><?= $index + 1 ?></span></h6>
<button type="button" class="btn btn-sm btn-outline-danger remove-row" <?= $index === 0 ? 'style="display:none"' : '' ?>>Remove</button>
</div>
<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"> Doctor Profile</p>
</div>
</header>
<main class="ov-content">
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="ov-panel" style="max-width: 1080px;">
<div class="ov-panel__header">
<h2 class="ov-panel__title"><?= $isEdit ? 'Edit Doctor Account' : 'Create Doctor Account' ?></h2>
<a href="<?= base_url('admin/doctors') ?>" class="btn btn-sm btn-outline-secondary px-3">Back to doctors</a>
</div>
<div class="ov-panel__body">
<span class="badge bg-light text-dark border mb-3 p-2">Fields marked with <span class="text-danger">*</span> are required.</span>
<form method="post" action="<?= $isEdit ? base_url('admin/doctors/edit/' . encrypt_id($user['id'])) : base_url('admin/doctors/add') ?>" class="app-form" novalidate>
<?= csrf_field() ?>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Full name</label>
<input type="text" name="doctors[<?= $index ?>][name]" value="<?= esc($doctor['name'] ?? '') ?>" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">Email (login)</label>
<input type="email" name="doctors[<?= $index ?>][email]" value="<?= esc($doctor['email'] ?? '') ?>" class="form-control" autocomplete="off">
<?php
echo view('components/name_field', [
'fieldName' => 'first_name',
'fieldLabel' => 'First name',
'fieldId' => 'first_name',
'fieldValue' => old('first_name', $first_name ?? ''),
'required' => true,
'validationErrors' => $validationErrors,
'placeholder' => 'Enter first name'
]);
?>
</div>
<div class="col-md-6">
<label class="form-label">Password</label>
<input type="password" name="doctors[<?= $index ?>][password]" value="<?= esc($doctor['password'] ?? '') ?>" class="form-control" autocomplete="new-password">
</div>
<div class="col-md-6">
<label class="form-label">Specialization</label>
<input type="text" name="doctors[<?= $index ?>][specialization]" value="<?= esc($doctor['specialization'] ?? '') ?>" class="form-control" placeholder="e.g. Cardiology">
<?php
echo view('components/name_field', [
'fieldName' => 'last_name',
'fieldLabel' => 'Last name',
'fieldId' => 'last_name',
'fieldValue' => old('last_name', $last_name ?? ''),
'required' => true,
'validationErrors' => $validationErrors,
'placeholder' => 'Enter last name'
]);
?>
</div>
<div class="col-md-4">
<label class="form-label">Experience (optional)</label>
<input type="text" name="doctors[<?= $index ?>][experience]" value="<?= esc($doctor['experience'] ?? '') ?>" class="form-control" placeholder="e.g. 10 years">
<div class="col-md-6">
<label class="form-label" for="email">Email <span class="text-danger">*</span></label>
<input type="email" name="email" id="email" onblur="checkEmail()" value="<?= esc(old('email', $user['email'] ?? '')) ?>" class="form-control <?= isset($validationErrors['email']) ? 'is-invalid' : '' ?>" autocomplete="off" placeholder="example@gmail.com" required>
<small id="emailError" class="text-danger"></small>
<?= validation_show_error('email') ?>
</div>
<div class="col-md-4">
<label class="form-label">Consultation fee (optional)</label>
<input type="number" name="doctors[<?= $index ?>][fees]" value="<?= esc($doctor['fees'] ?? '') ?>" class="form-control" placeholder="e.g. 500.00" step="0.01" min="0">
<div class="col-md-6">
<label class="form-label" for="specialization">Specialization <span class="text-danger">*</span></label>
<div class="multi-select-arrow-wrap">
<select name="specialization[]" id="specialization" class="form-select <?= isset($validationErrors['specialization']) ? 'is-invalid' : '' ?>" multiple required>
<?php foreach ($specializationOptions as $option): ?>
<option value="<?= esc($option) ?>" <?= in_array($option, $oldSpecializations, true) ? 'selected' : '' ?>>
<?= esc($option) ?>
</option>
<?php endforeach; ?>
<?php foreach ($oldSpecializations as $selected): ?>
<?php if (! in_array($selected, $specializationOptions, true)): ?>
<option value="<?= esc($selected) ?>" selected><?= esc($selected) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
<i class="bi bi-chevron-down select2-arrow-hint" aria-hidden="true"></i>
</div>
<?= validation_show_error('specialization') ?>
</div>
<div class="col-md-2">
<label class="form-label">Available from</label>
<input type="time" name="doctors[<?= $index ?>][available_from]" value="<?= esc($doctor['available_from'] ?? '') ?>" class="form-control">
<div class="col-md-6">
<label class="form-label" for="experience_years">Experience <span class="text-danger">*</span></label>
<div class="row g-0 experience-group">
<div class="col-6 ">
<div class="input-group">
<input type="number" name="experience_years" id="experience_years" value="<?= esc(old('experience_years', $experience_years ?? '')) ?>" class="form-control <?= isset($validationErrors['experience_years']) ? 'is-invalid' : '' ?>" min="0" max="60" placeholder="0" required>
<span class="input-group-text experience-unit">Years</span>
</div>
<?= validation_show_error('experience_years') ?>
</div>
<div class="col-6">
<div class="input-group">
<input type="number" name="experience_months" id="experience_months" value="<?= esc(old('experience_months', $experience_months ?? '')) ?>" class="form-control <?= isset($validationErrors['experience_months']) ? 'is-invalid' : '' ?>" min="0" max="11" placeholder="0" required>
<span class="input-group-text experience-unit">Months</span>
</div>
<?= validation_show_error('experience_months') ?>
</div>
</div>
</div>
<div class="col-md-2">
<label class="form-label">Available to</label>
<input type="time" name="doctors[<?= $index ?>][available_to]" value="<?= esc($doctor['available_to'] ?? '') ?>" class="form-control">
<div class="col-md-6">
<label class="form-label" for="fees">Consultation fee</label>
<input type="number" name="fees" id="fees" value="<?= esc(old('fees', $doctor['fees'] ?? '')) ?>" class="form-control <?= isset($validationErrors['fees']) ? 'is-invalid' : '' ?>" placeholder="e.g. 500.00" step="0.01" min="0">
<?= validation_show_error('fees') ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="d-flex flex-wrap gap-2 justify-content-between mt-4">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-outline-secondary rounded-pill px-4">Cancel</a>
<button type="submit" class="btn btn-app-primary px-4">Create doctors</button>
<div class="d-flex flex-wrap gap-4 justify-content-between mt-4">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-outline-secondary rounded-pill px-3">Cancel</a>
<button type="submit" class="btn btn-app-primary px-4 py-2"><?= $isEdit ? 'Update doctor' : 'Add doctor' ?></button>
</div>
</form>
</div>
</div>
</form>
</main>
</div>
<script>
(() => {
const rowsContainer = document.getElementById('doctor-rows');
const addBtn = document.getElementById('add-doctor-row');
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';
}
const updateRowNumbers = () => {
const rows = rowsContainer.querySelectorAll('[data-row]');
rows.forEach((row, index) => {
const numberEl = row.querySelector('[data-row-number]');
if (numberEl) numberEl.textContent = String(index + 1);
row.querySelectorAll('input').forEach((input) => {
const name = input.getAttribute('name');
if (!name) return;
input.setAttribute('name', name.replace(/doctors\[\d+]/, `doctors[${index}]`));
});
const removeBtn = row.querySelector('.remove-row');
if (removeBtn) {
removeBtn.style.display = rows.length === 1 ? 'none' : 'inline-block';
}
});
};
addBtn.addEventListener('click', () => {
const firstRow = rowsContainer.querySelector('[data-row]');
if (!firstRow) return;
const clone = firstRow.cloneNode(true);
clone.querySelectorAll('input').forEach((input) => {
input.value = '';
});
rowsContainer.appendChild(clone);
updateRowNumbers();
});
rowsContainer.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement) || !target.classList.contains('remove-row')) {
return;
}
const row = target.closest('[data-row]');
if (!row) return;
row.remove();
updateRowNumbers();
});
updateRowNumbers();
})();
function toggleNavDropdown(event, element) {
event.preventDefault();
element.parentElement.classList.toggle('active');
}
</script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function () {
const $specialization = $('#specialization');
const $specializationArrow = $('.select2-arrow-hint');
let isSpecializationOpen = false;
$specialization.select2({
placeholder: 'Select or type specializations',
tags: true,
width: '100%',
tokenSeparators: [',']
});
$specialization.on('select2:open', function () {
isSpecializationOpen = true;
$specializationArrow.removeClass('bi-chevron-down').addClass('bi-chevron-up');
});
$specialization.on('select2:close', function () {
isSpecializationOpen = false;
$specializationArrow.removeClass('bi-chevron-up').addClass('bi-chevron-down');
});
$specializationArrow.on('mousedown', function (event) {
event.preventDefault();
event.stopPropagation();
if (isSpecializationOpen) {
$specialization.select2('close');
return;
}
$specialization.select2('open');
});
});
function checkEmail() {
let email = document.getElementById("email").value;
let errorField = document.getElementById("emailError");
const excludeId = <?= $isEdit ? (int) ($user['id'] ?? 0) : 0 ?>;
if (email === "") {
errorField.innerText = "";
document.getElementById("email").classList.remove("is-invalid");
return;
}
fetch("<?= base_url('check-email') ?>", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "email=" + encodeURIComponent(email)
+ "&exclude_id=" + encodeURIComponent(excludeId)
+ "&<?= csrf_token() ?>=<?= csrf_hash() ?>",
})
.then(response => response.json())
.then(data => {
if (data.exists) {
errorField.innerText = "Email already exists!";
document.getElementById("email").classList.add("is-invalid");
} else {
errorField.innerText = "";
document.getElementById("email").classList.remove("is-invalid");
}
})
.catch(error => console.log(error));
}
</script>
</body>
</html>

View File

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Add Patient</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://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') ?>">
</head>
<body class="app-body overview-layout">
<?php $validationErrors = validation_errors(); ?>
<?php
$isEdit = $isEdit ?? false;
$patient = $patient ?? null;
$user = $user ?? null;
?>
<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 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>
<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"><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">Patient Profile</p>
</div>
</header>
<main class="ov-content">
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="ov-panel" style="max-width: 1080px;">
<div class="ov-panel__header">
<h2 class="ov-panel__title"><?= $isEdit ? 'Edit Patient Account' : 'Create Patient Account' ?></h2>
<a href="<?= base_url('admin/patients') ?>" class="btn btn-sm btn-outline-secondary px-3">Back to patients</a>
</div>
<div class="ov-panel__body">
<span class="badge bg-light text-dark border mb-3 p-2">Fields marked with <span class="text-danger">*</span> are required.</span>
<form method="post" action="<?= $isEdit ? base_url('admin/patients/edit/' . encrypt_id($user['id'])) : base_url('admin/patients/add') ?>" class="app-form" novalidate>
<?= csrf_field() ?>
<div class="row g-3">
<div class="col-md-6">
<?= view('components/name_field', [
'fieldName' => 'first_name',
'fieldLabel' => 'First name',
'fieldId' => 'first_name',
'fieldValue' => old('first_name', $first_name ?? ''),
'required' => true,
'validationErrors' => $validationErrors,
'placeholder' => 'Enter first name',
]) ?>
</div>
<div class="col-md-6">
<?= view('components/name_field', [
'fieldName' => 'last_name',
'fieldLabel' => 'Last name',
'fieldId' => 'last_name',
'fieldValue' => old('last_name', $last_name ?? ''),
'required' => true,
'validationErrors' => $validationErrors,
'placeholder' => 'Enter last name',
]) ?>
</div>
<div class="col-md-6">
<label class="form-label" for="email">Email <span class="text-danger">*</span></label>
<input type="email" name="email" id="email" onblur="checkEmail()" value="<?= esc(old('email', $user['email'] ?? '')) ?>" class="form-control <?= isset($validationErrors['email']) ? 'is-invalid' : '' ?>" autocomplete="off" placeholder="example@gmail.com" required>
<small id="emailError" class="text-danger"></small>
<?= validation_show_error('email') ?>
</div>
<div class="col-md-6">
<label class="form-label" for="phone">
Phone <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text">+91</span>
<input type="tel" name="phone" id="phone" pattern="[0-9]{10}" maxlength="10"
value="<?= esc(old('phone')) ?>"
class="form-control <?= isset($validationErrors['phone']) ? 'is-invalid' : '' ?>"
placeholder="Enter phone number"
autocomplete="tel"
required>
</div>
<?= validation_show_error('phone') ?>
</div>
<div class="col-12">
<?= view('components/password_field', ['id' => 'password']) ?>
</div>
<div class="col-md-6">
<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">
<label class="form-label" for="gender">Gender</label>
<?php $selectedGender = old('gender', $patient['gender'] ?? ''); ?>
<select name="gender" id="gender" class="form-select <?= isset($validationErrors['gender']) ? 'is-invalid' : '' ?>">
<option value="">Select gender</option>
<option value="male" <?= $selectedGender === 'male' ? 'selected' : '' ?>>Male</option>
<option value="female" <?= $selectedGender === 'female' ? 'selected' : '' ?>>Female</option>
<option value="other" <?= $selectedGender === 'other' ? 'selected' : '' ?>>Other</option>
</select>
<?= validation_show_error('gender') ?>
</div>
</div>
<div class="d-flex flex-wrap gap-4 justify-content-between mt-4">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-outline-secondary rounded-pill px-3">Cancel</a>
<button type="submit" class="btn btn-app-primary px-4 py-2"><?= $isEdit ? 'Update patient' : 'Add patient' ?></button>
</div>
</form>
</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';
}
function toggleNavDropdown(event, element) {
event.preventDefault();
element.parentElement.classList.toggle('active');
}
function checkEmail() {
const email = document.getElementById('email').value;
const errorField = document.getElementById('emailError');
const excludeId = <?= $isEdit ? (int) ($user['id'] ?? 0) : 0 ?>;
if (email === '') {
errorField.innerText = '';
document.getElementById('email').classList.remove('is-invalid');
return;
}
fetch("<?= base_url('check-email') ?>", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "email=" + encodeURIComponent(email)
+ "&exclude_id=" + encodeURIComponent(excludeId)
+ "&<?= csrf_token() ?>=<?= csrf_hash() ?>",
})
.then(response => response.json())
.then(data => {
if (data.exists) {
errorField.innerText = "Email already exists!";
document.getElementById('email').classList.add('is-invalid');
} else {
errorField.innerText = '';
document.getElementById('email').classList.remove('is-invalid');
}
})
.catch(error => console.log(error));
}
</script>
</body>
</html>

View File

@ -5,50 +5,103 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Appointments</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 app-page--admin">
<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 active"><i class="bi bi-calendar2-check"></i> Appointments</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>
<div class="container py-5">
<h2 class="text-center mb-4 app-heading">Appointments</h2>
<div class="app-table-wrap">
<table class="table table-striped table-hover text-center align-middle">
<thead>
<tr>
<th>Sl No</th>
<th>Patient</th>
<th>Doctor</th>
<th>Date</th>
<th>Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php $i = 1; ?>
<?php foreach ($appointments as $a): ?>
<tr>
<td><?= $i++ ?></td>
<td><?= esc($a->patient_name) ?></td>
<td><?= esc($a->doctor_name) ?></td>
<td><?= esc($a->appointment_date) ?></td>
<td><?= esc($a->appointment_time) ?></td>
<?php $status = trim((string) ($a->status ?? '')); ?>
<td><?= esc(ucfirst($status === '' ? 'pending' : $status)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="text-center mt-4">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-app-outline px-4">Back to dashboard</a>
</div>
<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">Appointments</p>
</div>
</header>
<main class="ov-content">
<div class="ov-panel">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Appointments</h2>
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-sm btn-outline-secondary px-3">Back to dashboard</a>
</div>
<div class="p-0">
<table class="table ov-mini-table mb-0">
<thead>
<tr>
<th class="ps-3">Sl No</th>
<th>Patient</th>
<th>Doctor</th>
<th>Date</th>
<th>Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php $i = 1; ?>
<?php foreach ($appointments as $a): ?>
<?php $status = trim((string) ($a->status ?? '')); ?>
<tr>
<td class="ps-3"><?= $i++ ?></td>
<td><?= esc($a->patient_name) ?></td>
<td><?= esc($a->doctor_name) ?></td>
<td><?= esc($a->appointment_date) ?></td>
<td><?= esc($a->appointment_time) ?></td>
<td><?= esc(ucfirst($status === '' ? 'pending' : $status)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</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';
}
function toggleNavDropdown(event, element) {
event.preventDefault();
element.parentElement.classList.toggle('active');
}
</script>
</body>
</html>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!-- <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
@ -54,5 +54,376 @@
</div>
</body>
</html> -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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">
<!-- Sidebar -->
<aside class="ov-sidebar" id="sidebar">
<div class="ov-brand">
<h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1>
<span>Control Panel</span>
</div>
<nav class="ov-nav">
<div class="ov-nav__section">Main</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link active">
<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">
<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>
<!-- Main -->
<div class="ov-main" id="mainContent">
<!-- Topbar -->
<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">Dashboard</p>
</div>
<!-- Profile dropdown -->
<div class="ov-profile" id="profileMenu">
<button class="ov-profile__btn" onclick="toggleDropdown()">
<div class="ov-avatar">A</div>
<span class="ov-profile__name">Admin</span>
<i class="bi bi-chevron-down ov-profile__caret"></i>
</button>
<div class="ov-dropdown" id="profileDropdown">
<div class="ov-dropdown__header">
<p class="ov-dropdown__name"><?= esc($adminName ?? 'Administrator') ?></p>
<span class="ov-dropdown__role"><?= esc($adminEmail ?? '') ?></span>
</div>
<a href="<?= base_url('admin/dashboard') ?>" class="ov-dropdown__item">
<i class="bi bi-grid-1x2"></i> Dashboard
</a>
<a href="<?= base_url('admin/appointments') ?>" class="ov-dropdown__item">
<i class="bi bi-calendar2-check"></i> Appointments
</a>
<hr class="ov-dropdown__divider">
<a href="<?= base_url('logout') ?>" class="ov-dropdown__item danger">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>
</div>
</header>
<!-- Content -->
<main class="ov-content">
<!-- Stat cards -->
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3">
<div class="ov-stat">
<div class="ov-stat__icon"><i class="bi bi-person-badge"></i></div>
<div>
<div class="ov-stat__label">Doctors</div>
<p class="ov-stat__value"><?= esc($totalDoctors) ?></p>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="ov-stat">
<div class="ov-stat__icon"><i class="bi bi-people"></i></div>
<div>
<div class="ov-stat__label">Patients</div>
<p class="ov-stat__value"><?= esc($totalPatients) ?></p>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="ov-stat">
<div class="ov-stat__icon"><i class="bi bi-calendar2-check"></i></div>
<div>
<div class="ov-stat__label">Appointments</div>
<p class="ov-stat__value"><?= esc($totalAppointments) ?></p>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="ov-stat">
<div class="ov-stat__icon"><i class="bi bi-activity"></i></div>
<div>
<div class="ov-stat__label">Active Today</div>
<p class="ov-stat__value"><?= esc($activeToday ?? 0) ?></p>
</div>
</div>
</div>
</div>
<!-- Quick actions + Activity -->
<div class="row g-3 mb-4">
<!-- Quick actions -->
<div class="col-lg-4">
<div class="ov-panel h-100">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Quick Actions</h2>
</div>
<div class="ov-panel__body">
<div class="row g-2">
<div class="col-6">
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-action">
<i class="bi bi-person-plus"></i> Add Doctor
</a>
</div>
<div class="col-6">
<a href="<?= base_url('admin/doctors') ?>" class="ov-action">
<i class="bi bi-person-lines-fill"></i> Doctors
</a>
</div>
<div class="col-6">
<a href="<?= base_url('admin/patients') ?>" class="ov-action">
<i class="bi bi-people"></i> Patients
</a>
</div>
<div class="col-6">
<a href="<?= base_url('admin/appointments') ?>" class="ov-action">
<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>
</div>
<!-- Activity feed -->
<div class="col-lg-8">
<div class="ov-panel h-100">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Recent Activity</h2>
</div>
<div class="ov-panel__body">
<?php if (!empty($recentActivity)) : ?>
<?php foreach ($recentActivity as $activity) : ?>
<?php
$status = strtolower($activity['status'] ?? 'pending');
$dotColor = match($status) {
'approved' => '#22c55e',
'rejected' => '#ef4444',
default => '#eab308',
};
$badgeClass = match($status) {
'approved' => 'ov-badge--success',
'rejected' => 'ov-badge--danger',
default => 'ov-badge--warning',
};
$badgeLabel = ucfirst($status);
?>
<div class="ov-activity-item">
<div class="ov-activity-dot" style="background:<?= $dotColor ?>;"></div>
<div>
<div class="ov-activity-text">
<strong><?= esc($activity['patient_name']) ?></strong>
booked with
<strong><?= esc($activity['doctor_name']) ?></strong>
<span class="ov-badge <?= $badgeClass ?> ms-1"><?= $badgeLabel ?></span>
</div>
<div class="ov-activity-time"><?= esc($activity['appointment_date']) ?> &middot; <?= esc($activity['appointment_time']) ?></div>
</div>
</div>
<?php endforeach; ?>
<?php else : ?>
<p class="text-muted" style="font-size:0.83rem;">No recent activity.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Doctors + Patients tables -->
<div class="row g-3">
<div class="col-lg-6">
<div class="ov-panel">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Doctors</h2>
<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 id="dashboardDoctorsTable" class="table ov-mini-table mb-0">
<thead>
<tr>
<th class="ps-3">#User ID</th>
<th>Name</th>
<th>Specialization</th>
</tr>
</thead>
<tbody>
<?php if (!empty($latestDoctors)) : ?>
<?php foreach ($latestDoctors as $doctor) : ?>
<tr>
<td class="ps-3"><?= esc($doctor['formatted_user_id'] ?? 'N/A') ?></td>
<td>Dr. <?= esc($doctor['name']) ?></td>
<td><?= esc($doctor['specialization']) ?></td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr><td colspan="3" class="ps-3 text-muted">No doctors found.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="ov-panel">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Patients</h2>
<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 id="dashboardPatientsTable" class="table ov-mini-table mb-0">
<thead>
<tr>
<th class="ps-3">#User ID</th>
<th>Name</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<?php if (!empty($latestPatients)) : ?>
<?php foreach ($latestPatients as $patient) : ?>
<tr>
<td class="ps-3"><?= esc($patient['formatted_user_id'] ?? 'N/A') ?></td>
<td><?= esc($patient['name']) ?></td>
<td><?= esc($patient['phone']) ?></td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr><td colspan="3" class="ps-3 text-muted">No patients found.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</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
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');
if (sidebar.classList.contains('collapsed')) {
icon.className = 'bi bi-layout-sidebar';
} else {
icon.className = 'bi bi-list';
}
}
function toggleNavDropdown(event, element) {
event.preventDefault();
element.parentElement.classList.toggle('active');
}
// Profile dropdown
function toggleDropdown() {
document.getElementById('profileDropdown').classList.toggle('open');
}
document.addEventListener('click', function (e) {
const menu = document.getElementById('profileMenu');
if (!menu.contains(e.target)) {
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>

View File

@ -5,64 +5,356 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Doctors</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') ?>">
<link rel="stylesheet" href="<?= base_url('css/doctors.css') ?>">
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
</head>
<body class="app-body app-page--admin">
<div class="container py-5">
<h2 class="text-center mb-4 app-heading">Doctors</h2>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert text-center"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="text-center mb-4">
<a href="<?= base_url('admin/doctors/add') ?>" class="btn btn-app-primary">Add doctor</a>
<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>
<div class="app-table-wrap">
<table class="table table-bordered table-hover text-center align-middle">
<thead>
<tr>
<th>Sl No</th>
<th>Name</th>
<th>Specialization</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php $i = 1; ?>
<?php foreach ($doctors as $doc): ?>
<tr>
<td><?= $i++ ?></td>
<td><?= esc($doc->name) ?></td>
<td><?= esc($doc->specialization ?? 'N/A') ?></td>
<td>
<a href="<?= base_url('admin/deleteDoctor/' . $doc->id) ?>"
class="btn btn-outline-danger btn-sm rounded-pill px-3"
onclick="return confirm('Delete this doctor?');">
Delete
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<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 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>
<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"><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="text-center mt-4">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-app-outline px-4">Back to dashboard</a>
</div>
<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">Doctors</p>
</div>
</header>
<main class="ov-content">
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="ov-panel">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Doctor List</h2>
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" onclick="exportTable('csv'); return false;"><i class="bi bi-file-earmark-text me-2"></i> CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportTable('excel'); return false;"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
<li><a class="dropdown-item" href="#" onclick="exportTable('pdf'); return false;"><i class="bi bi-file-earmark-pdf me-2"></i> PDF</a></li>
</ul>
</div>
</div>
</div>
<div class="p-0">
<table id="doctorsTable" class="table table-hover" style="width:100%">
<thead class="table-light">
<tr>
<th width="60">User ID</th>
<th>Doctor Name</th>
<th>Email</th>
<th>Specialization</th>
<th>Experience</th>
<th>Consultation Fee</th>
<th width="120">Status</th>
<th width="130">Actions</th>
</tr>
</thead>
</table>
</div>
</div>
</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.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.print.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let doctorsTable = null;
const expandedSpecializations = new Set();
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function renderSpecializations(data, row) {
if (!data) {
return '<span class="text-muted">N/A</span>';
}
const specs = String(data)
.split(',')
.map(spec => spec.trim())
.filter(Boolean);
let html = specs.slice(0, 2).map(spec =>
`<span class="badge bg-light text-dark me-1">${escapeHtml(spec)}</span>`
).join('');
if (specs.length > 2) {
const extraSpecializations = specs.slice(2);
const extraId = `extra-spec-${escapeHtml(row.user_id ?? '')}`;
const isExpanded = expandedSpecializations.has(extraId);
const hiddenClass = isExpanded ? '' : ' d-none';
const outerToggleHiddenClass = isExpanded ? ' d-none' : '';
const innerToggleHiddenClass = isExpanded ? '' : ' d-none';
const toggleLabel = isExpanded ? 'Show less' : `+${extraSpecializations.length} more`;
html += `<div id="${extraId}" class="extra-specializations mt-2${hiddenClass}">`;
html += extraSpecializations.map(spec =>
`<span class="badge bg-light text-dark me-1 mb-1">${escapeHtml(spec)}</span>`
).join('');
html += `<button type="button" class="badge bg-secondary border-0 specialization-toggle ms-1 mb-1${innerToggleHiddenClass}" data-target="${extraId}" data-show-label="+${extraSpecializations.length} more" data-hide-label="Show less">Show less</button>`;
html += `</div>`;
html += `<button type="button" class="badge bg-secondary border-0 specialization-toggle${outerToggleHiddenClass}" data-target="${extraId}" data-show-label="+${extraSpecializations.length} more" data-hide-label="Show less">${toggleLabel}</button>`;
}
return html;
}
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';
}
function toggleNavDropdown(event, element) {
event.preventDefault();
element.parentElement.classList.toggle('active');
}
$(document).ready(function() {
doctorsTable = $('#doctorsTable').DataTable({
processing: false,
ajax: {
url: "<?= base_url('admin/doctors/data') ?>",
type: 'GET',
dataSrc: 'data'
},
columns: [
{
data: 'formatted_user_id',
defaultContent: 'N/A'
},
{
data: 'name',
render: function (data) {
return `<div class="fw-medium">${escapeHtml(data ?? 'Unknown Doctor')}</div>`;
}
},
{
data: 'email',
render: function (data) {
if (!data) {
return '<span class="text-muted">N/A</span>';
}
const safeEmail = escapeHtml(data);
return `<a href="mailto:${safeEmail}" class="text-decoration-none">${safeEmail}</a>`;
}
},
{
data: 'specialization',
render: function (data, type, row) {
return renderSpecializations(data, row);
}
},
{
data: 'experience',
render: function (data) {
return data ? escapeHtml(data) : '<span class="text-muted">N/A</span>';
}
},
{
data: 'fees',
render: function (data) {
return data && parseFloat(data) > 0
? parseFloat(data).toFixed(2)
: '<span class="text-muted">N/A</span>';
}
},
{
data: 'status',
render: function (data) {
const status = String(data ?? 'active').toLowerCase();
const statusClass = status === 'active' ? 'success' : 'secondary';
const label = status.charAt(0).toUpperCase() + status.slice(1);
return `
<span class="badge bg-${statusClass} rounded-pill">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
${escapeHtml(label)}
</span>
`;
}
},
{
data: null,
orderable: false,
searchable: false,
render: function (data, type, row) {
const editUrl = "<?= base_url('admin/doctors/edit') ?>/" + encodeURIComponent(row.edit_token);
const deleteUrl = "<?= base_url('admin/deleteDoctor') ?>/" + encodeURIComponent(row.user_id);
return `
<div class="btn-group" role="group">
<a href="${editUrl}" class="btn btn-outline-primary btn-sm" title="Edit Doctor">
<i class="bi bi-pencil"></i>
</a>
<a href="${deleteUrl}" class="btn btn-outline-danger btn-sm" title="Delete Doctor"
onclick="return confirm('Are you sure you want to delete this doctor? This action cannot be undone.');">
<i class="bi bi-trash"></i>
</a>
</div>
`;
}
}
],
pageLength: 10,
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
order: [[0, 'asc']],
dom: '<"row mb-3"<"col-md-6 d-flex align-items-center"l><"col-md-6 d-flex justify-content-end"f>>rtip',
buttons: [
{
extend: 'csvHtml5',
className: 'buttons-csv',
title: 'Doctors',
filename: 'doctors_list_' + new Date().toISOString().split('T')[0],
exportOptions: {
columns: [0, 1, 2, 3, 4, 5, 6]
}
},
{
extend: 'excelHtml5',
className: 'buttons-excel',
title: 'Doctors',
filename: 'doctors_list_' + new Date().toISOString().split('T')[0],
exportOptions: {
columns: [0, 1, 2, 3, 4, 5, 6]
}
},
{
extend: 'pdfHtml5',
className: 'buttons-pdf',
title: 'Doctors List',
filename: 'doctors_list_' + new Date().toISOString().split('T')[0],
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: [0, 1, 2, 3, 4, 5, 6]
}
}
],
language: {
search: 'Search doctors:',
lengthMenu: 'Show _MENU_ doctors',
info: 'Showing _START_ to _END_ of _TOTAL_ doctors',
paginate: {
first: 'First',
last: 'Last',
next: 'Next',
previous: 'Previous'
},
emptyTable: 'No doctors found',
zeroRecords: 'No matching doctors found'
}
});
setInterval(function () {
if (doctorsTable) {
doctorsTable.ajax.reload(null, false);
}
}, 5000);
});
function exportTable(format) {
if (!doctorsTable) {
return;
}
if (format === 'csv') {
doctorsTable.button('.buttons-csv').trigger();
} else if (format === 'excel') {
doctorsTable.button('.buttons-excel').trigger();
} else if (format === 'pdf') {
doctorsTable.button('.buttons-pdf').trigger();
}
}
$(document).on('click', '.specialization-toggle', function () {
const targetId = $(this).data('target');
const showLabel = $(this).data('show-label');
const hideLabel = $(this).data('hide-label');
const $target = $('#' + targetId);
const $allToggles = $('.specialization-toggle[data-target="' + targetId + '"]');
$target.toggleClass('d-none');
if ($target.hasClass('d-none')) {
expandedSpecializations.delete(targetId);
} else {
expandedSpecializations.add(targetId);
}
$allToggles.each(function () {
const isInsideExpandedArea = $(this).closest('.extra-specializations').length > 0;
$(this).toggleClass('d-none', $target.hasClass('d-none') ? isInsideExpandedArea : !isInsideExpandedArea);
});
$allToggles.text($target.hasClass('d-none') ? showLabel : hideLabel);
});
</script>
</body>
</html>

View File

@ -5,53 +5,276 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Patients</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') ?>">
<link rel="stylesheet" href="<?= base_url('css/doctors.css') ?>">
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
</head>
<body class="app-body app-page--admin">
<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 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>
<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"><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="container py-5">
<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">Patients</p>
</div>
</header>
<h2 class="text-center mb-4 app-heading">Patients</h2>
<div class="app-table-wrap">
<table class="table table-bordered table-hover text-center align-middle">
<thead>
<tr>
<th>Sl No</th>
<th>Name</th>
<th>Phone</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php $i = 1; ?>
<?php foreach ($patients as $p): ?>
<tr>
<td><?= $i++ ?></td>
<td><?= esc($p->name) ?></td>
<td><?= esc($p->phone ?? 'N/A') ?></td>
<td>
<a href="<?= base_url('admin/deletePatient/' . $p->id) ?>"
class="btn btn-outline-danger btn-sm rounded-pill px-3"
onclick="return confirm('Delete this patient?');">
Delete
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="text-center mt-4">
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-app-outline px-4">Back to dashboard</a>
</div>
<main class="ov-content">
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="ov-panel">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Patient List</h2>
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" onclick="exportTable('csv'); return false;"><i class="bi bi-file-earmark-text me-2"></i> CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportTable('excel'); return false;"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
<li><a class="dropdown-item" href="#" onclick="exportTable('pdf'); return false;"><i class="bi bi-file-earmark-pdf me-2"></i> PDF</a></li>
</ul>
</div>
</div>
</div>
<div class="p-0">
<table id="patientsTable" class="table table-hover" style="width:100%">
<thead class="table-light">
<tr>
<th width="60">User ID</th>
<th>Patient Name</th>
<th>Email</th>
<th>Phone</th>
<th width="120">Status</th>
<th width="130">Actions</th>
</tr>
</thead>
</table>
</div>
</div>
</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.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.print.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let patientsTable = null;
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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';
}
function toggleNavDropdown(event, element) {
event.preventDefault();
element.parentElement.classList.toggle('active');
}
$(document).ready(function() {
patientsTable = $('#patientsTable').DataTable({
processing: false,
ajax: {
url: "<?= base_url('admin/patients/data') ?>",
type: 'GET',
dataSrc: 'data'
},
columns: [
{
data: 'formatted_user_id',
defaultContent: 'N/A'
},
{
data: 'name',
render: function (data) {
return `<div class="fw-medium">${escapeHtml(data ?? 'Unknown Patient')}</div>`;
}
},
{
data: 'email',
render: function (data) {
if (!data) {
return '<span class="text-muted">N/A</span>';
}
const safeEmail = escapeHtml(data);
return `<a href="mailto:${safeEmail}" class="text-decoration-none">${safeEmail}</a>`;
}
},
{
data: 'phone',
render: function (data) {
return data ? escapeHtml(data) : '<span class="text-muted">N/A</span>';
}
},
{
data: 'status',
render: function (data) {
const status = String(data ?? 'active').toLowerCase();
const statusClass = status === 'active' ? 'success' : 'secondary';
const label = status.charAt(0).toUpperCase() + status.slice(1);
return `
<span class="badge bg-${statusClass} rounded-pill">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
${escapeHtml(label)}
</span>
`;
}
},
{
data: null,
orderable: false,
searchable: false,
render: function (data, type, row) {
const editUrl = "<?= base_url('admin/patients/edit') ?>/" + encodeURIComponent(row.edit_token);
const deleteUrl = "<?= base_url('admin/deletePatient') ?>/" + encodeURIComponent(row.user_id);
return `
<div class="btn-group" role="group">
<a href="${editUrl}" class="btn btn-outline-primary btn-sm" title="Edit Patient">
<i class="bi bi-pencil"></i>
</a>
<a href="${deleteUrl}" class="btn btn-outline-danger btn-sm" title="Delete Patient"
onclick="return confirm('Are you sure you want to delete this patient? This action cannot be undone.');">
<i class="bi bi-trash"></i>
</a>
</div>
`;
}
}
],
pageLength: 10,
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
order: [[0, 'asc']],
dom: '<"row mb-3"<"col-md-6 d-flex align-items-center"l><"col-md-6 d-flex justify-content-end"f>>rtip',
buttons: [
{
extend: 'csvHtml5',
className: 'buttons-csv',
title: 'Patients',
filename: 'patients_list_' + new Date().toISOString().split('T')[0],
exportOptions: {
columns: [0, 1, 2, 3, 4]
}
},
{
extend: 'excelHtml5',
className: 'buttons-excel',
title: 'Patients',
filename: 'patients_list_' + new Date().toISOString().split('T')[0],
exportOptions: {
columns: [0, 1, 2, 3, 4]
}
},
{
extend: 'pdfHtml5',
className: 'buttons-pdf',
title: 'Patients List',
filename: 'patients_list_' + new Date().toISOString().split('T')[0],
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: [0, 1, 2, 3, 4]
}
}
],
language: {
search: 'Search patients:',
lengthMenu: 'Show _MENU_ patients',
info: 'Showing _START_ to _END_ of _TOTAL_ patients',
paginate: {
first: 'First',
last: 'Last',
next: 'Next',
previous: 'Previous'
},
emptyTable: 'No patients found',
zeroRecords: 'No matching patients found'
}
});
setInterval(function () {
if (patientsTable) {
patientsTable.ajax.reload(null, false);
}
}, 5000);
});
function exportTable(format) {
if (!patientsTable) {
return;
}
if (format === 'csv') {
patientsTable.button('.buttons-csv').trigger();
} else if (format === 'excel') {
patientsTable.button('.buttons-excel').trigger();
} else if (format === 'pdf') {
patientsTable.button('.buttons-pdf').trigger();
}
}
</script>
</body>
</html>

View File

@ -6,6 +6,7 @@
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<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>
<body class="app-body app-page--auth">
@ -35,9 +36,13 @@
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" id="password"
class="form-control <?= isset($validationErrors['password']) ? 'is-invalid' : '' ?>"
placeholder="Enter password" autocomplete="current-password" required>
<div class="password-container">
<input type="password" name="password" id="password"
class="form-control <?= isset($validationErrors['password']) ? 'is-invalid' : '' ?>"
placeholder="At least 8 characters" autocomplete="new-password"
minlength="8" required>
<span class="toggle-password" onclick="togglePassword()"><i id="eyeIcon" class="fa fa-eye"></i></span>
</div>
<?= validation_show_error('password') ?>
</div>
@ -54,6 +59,6 @@
</div>
</div>
</div>
<script src="<?= base_url('js/script.js') ?>"></script>
</body>
</html>

View File

@ -5,7 +5,10 @@
<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>
<body class="app-body app-page--auth">
@ -23,15 +26,35 @@
<?= csrf_field() ?>
<div class="mb-3">
<label for="name" class="form-label">Full name</label>
<input type="text" name="name" id="name" value="<?= esc(old('name')) ?>"
class="form-control <?= isset($validationErrors['name']) ? 'is-invalid' : '' ?>"
placeholder="Your name" minlength="3" maxlength="100" required>
<?= validation_show_error('name') ?>
<?php
echo view('components/name_field', [
'fieldName' => 'first_name',
'fieldLabel' => 'First name',
'fieldId' => 'first_name',
'fieldValue' => old('first_name', $first_name ?? ''),
'required' => true,
'validationErrors' => $validationErrors,
'placeholder' => 'Enter first name'
]);
?>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<?php
echo view('components/name_field', [
'fieldName' => 'last_name',
'fieldLabel' => 'Last name',
'fieldId' => 'last_name',
'fieldValue' => old('last_name', $last_name ?? ''),
'required' => true,
'validationErrors' => $validationErrors,
'placeholder' => 'Enter last name'
]);
?>
</div>
<div class="mb-3">
<label class="form-label" for="email">Email <span class="text-danger">*</span></label>
<input type="email" name="email" id="email" value="<?= esc(old('email')) ?>"
class="form-control <?= isset($validationErrors['email']) ? 'is-invalid' : '' ?>"
placeholder="Email address" autocomplete="email" required>
@ -39,23 +62,27 @@
</div>
<div class="mb-3">
<label for="phone" class="form-label">Phone</label>
<input type="phone" name="phone" id="phone" value="<?= esc(old('phone')) ?>"
class="form-control <?= isset($validationErrors['phone']) ? 'is-invalid' : '' ?>"
placeholder="Phone number" autocomplete="tel" required>
<?= validation_show_error('phone') ?>
</div>
<label class="form-label" for="phone">
Phone <span class="text-danger">*</span>
</label>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" id="password"
class="form-control <?= isset($validationErrors['password']) ? 'is-invalid' : '' ?>"
placeholder="At least 8 characters" autocomplete="new-password"
minlength="8" required>
<?= validation_show_error('password') ?>
</div>
<div class="input-group">
<span class="input-group-text">+91</span>
<p class="small text-muted mb-4">Register as a <strong>patient</strong> to book appointments. Doctor accounts are created by an administrator.</p>
<input type="tel" name="phone" id="phone" pattern="[0-9]{10}" maxlength="10"
value="<?= esc(old('phone')) ?>"
class="form-control <?= isset($validationErrors['phone']) ? 'is-invalid' : '' ?>"
placeholder="Enter phone number"
autocomplete="tel"
required>
</div>
<?= validation_show_error('phone') ?>
</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>
<button type="submit" class="btn btn-app-primary w-100">Register</button>
</form>
@ -66,6 +93,5 @@
</div>
</div>
</div>
</body>
</html>

View File

@ -26,14 +26,27 @@
<input type="hidden" name="token" value="<?= esc($token) ?>">
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<input type="password" name="password" id="password" class="form-control"
placeholder="Enter new password (min 8 characters)" required>
<?php if (session()->has('errors.password')): ?>
<div class="text-danger small mt-1"><?= session('errors.password') ?></div>
<?php endif; ?>
</div>
<label for="password" class="form-label">New Password</label>
<input type="password" name="password" id="password" class="form-control"
placeholder="Enter strong password" required>
<!-- 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>
<?php if (session()->has('errors.password')): ?>
<div class="text-danger small mt-1"><?= session('errors.password') ?></div>
<?php endif; ?>
</div>
<button type="submit" class="btn btn-primary w-100">Reset Password</button>
</form>
@ -44,6 +57,67 @@
</div>
</div>
</div>
<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;
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");
}
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";
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,181 @@
<?php
$fieldName = $fieldName ?? '';
$fieldLabel = $fieldLabel ?? '';
$fieldId = $fieldId ?? $fieldName;
$fieldValue = $fieldValue ?? '';
$required = $required ?? true;
$validationErrors = $validationErrors ?? [];
$placeholder = $placeholder ?? "Enter " . strtolower($fieldLabel);
$hasError = isset($validationErrors[$fieldName]);
?>
<div class="name-field-component">
<label class="form-label" for="<?= esc($fieldId) ?>">
<?= esc($fieldLabel) ?>
<?php if ($required): ?><span class="text-danger">*</span><?php endif; ?>
</label>
<input
type="text"
name="<?= esc($fieldName) ?>"
id="<?= esc($fieldId) ?>"
value="<?= esc($fieldValue) ?>"
class="form-control name-input <?= $hasError ? 'is-invalid' : '' ?>"
placeholder="<?= esc($placeholder) ?>"
data-field-name="<?= esc($fieldName) ?>"
data-label="<?= esc($fieldLabel) ?>"
<?php if ($required): ?>required<?php endif; ?>
oninput="validateNameField(this)"
onblur="validateNameField(this)"
>
<div class="invalid-feedback" id="<?= esc($fieldId) ?>_error">
<?= $hasError ? esc($validationErrors[$fieldName]) : '' ?>
</div>
</div>
<style>
.name-field-component {
position: relative;
}
.name-input.is-invalid {
border-color: #dc3545;
}
.invalid-feedback {
display: none;
font-size: 0.875em;
color: #dc3545;
margin-top: 0.25rem;
}
.invalid-feedback.show {
display: block;
}
.name-field-component .text-muted {
font-size: 0.75rem;
margin-top: 0.25rem;
display: block;
}
.name-input.is-invalid ~ .text-muted {
display: none;
}
</style>
<script>
function validateNameField(input) {
const fieldName = input.dataset.fieldName;
const fieldLabel = input.dataset.label;
let value = input.value.trim();
const errorElement = document.getElementById(input.id + '_error');
const hintElement = document.getElementById(input.id + '_hint');
// Clear previous validation state
input.classList.remove('is-invalid');
errorElement.classList.remove('show');
errorElement.textContent = '';
if (value === '') {
// Field is empty, check if required
if (input.hasAttribute('required')) {
input.classList.add('is-invalid');
errorElement.textContent = fieldLabel + ' is required';
errorElement.classList.add('show');
hintElement.style.display = 'none';
} else {
hintElement.style.display = 'block';
}
return false;
}
// Check for valid characters (letters only, including spaces for names)
const nameRegex = /^[a-zA-Z\s'-]+$/;
if (!nameRegex.test(value)) {
input.classList.add('is-invalid');
errorElement.textContent = fieldLabel + ' can only contain letters, spaces, hyphens, and apostrophes';
errorElement.classList.add('show');
hintElement.style.display = 'none';
return false;
}
// Check minimum length
if (value.length < 2) {
input.classList.add('is-invalid');
errorElement.textContent = fieldLabel + ' must be at least 2 characters long';
errorElement.classList.add('show');
hintElement.style.display = 'none';
return false;
}
// Check maximum length
if (value.length > 50) {
input.classList.add('is-invalid');
errorElement.textContent = fieldLabel + ' cannot exceed 50 characters';
errorElement.classList.add('show');
hintElement.style.display = 'none';
return false;
}
// Auto-capitalize first letter of each word
const words = value.toLowerCase().split(/\s+/);
const capitalizedWords = words.map(word => {
if (word.length === 0) return '';
return word.charAt(0).toUpperCase() + word.slice(1);
});
const capitalizedValue = capitalizedWords.join(' ');
// Update input value if different
if (input.value !== capitalizedValue) {
input.value = capitalizedValue;
}
// Hide hint and show success
hintElement.style.display = 'none';
input.classList.remove('is-invalid');
input.classList.add('is-valid');
// Remove is-valid class after a short delay
setTimeout(() => {
input.classList.remove('is-valid');
}, 2000);
return true;
}
// Prevent non-letter characters on keypress
document.addEventListener('DOMContentLoaded', function() {
const nameInputs = document.querySelectorAll('.name-input');
nameInputs.forEach(input => {
input.addEventListener('keypress', function(e) {
const char = String.fromCharCode(e.which || e.keyCode);
const isAllowed = /^[a-zA-Z\s'-]$/.test(char);
if (!isAllowed) {
e.preventDefault();
return false;
}
});
// Prevent paste of invalid characters
input.addEventListener('paste', function(e) {
e.preventDefault();
const pastedData = (e.clipboardData || window.clipboardData).getData('text');
const cleanedData = pastedData.replace(/[^a-zA-Z\s'-]/g, '');
// Insert cleaned data at cursor position
const start = this.selectionStart;
const end = this.selectionEnd;
const currentValue = this.value;
const newValue = currentValue.substring(0, start) + cleanedData + currentValue.substring(end);
this.value = newValue;
this.selectionStart = this.selectionEnd = start + cleanedData.length;
validateNameField(this);
});
});
});
</script>

View File

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

View File

@ -5,38 +5,62 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Doctor 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="<?= base_url('css/app.css') ?>">
<link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>">
</head>
<body class="app-body app-page--doctor">
<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>Doctor Panel</span></div>
<nav class="ov-nav">
<div class="ov-nav__section">Doctor</div>
<a href="<?= base_url('doctor/dashboard') ?>" class="ov-nav__link active"><i class="bi bi-speedometer2"></i> Dashboard</a>
<a href="<?= base_url('doctor/profile') ?>" class="ov-nav__link"><i class="bi bi-person-gear"></i> Profile</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="container py-5">
<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">Doctor Dashboard</p>
</div>
<!-- <a href="<?= base_url('doctor/profile') ?>" class="btn btn-sm btn-outline-secondary px-3">Edit profile</a> -->
<div class="ov-profile" id="profileMenu">
<button class="ov-profile__btn" onclick="toggleDropdown()">
<div class="ov-avatar">D</div>
<span class="ov-profile__name">Doctor</span>
<i class="bi bi-chevron-down ov-profile__caret"></i>
</button>
<h2 class="text-center mb-4 app-heading">Doctor dashboard</h2>
<div class="ov-dropdown" id="profileDropdown">
<div class="ov-dropdown__header">
<p class="ov-dropdown__name"><?= esc($adminName ?? 'Administrator') ?></p>
<span class="ov-dropdown__role"><?= esc($adminEmail ?? '') ?></span>
</div>
<a href="<?= base_url('doctor/profile') ?>" class="ov-dropdown__item">
<i class="bi bi-grid-1x2"></i> Edit Profile
</a>
<hr class="ov-dropdown__divider">
<a href="<?= base_url('logout') ?>" class="ov-dropdown__item danger">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>
</div>
</header>
<p class="text-center mb-4">
<a href="<?= base_url('doctor/profile') ?>" class="btn btn-outline-primary btn-sm rounded-pill px-3">Edit profile</a>
</p>
<main class="ov-content">
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert text-center"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="row">
<?php foreach ($appointments as $a): ?>
<div class="col-md-6 mb-4">
<div class="card app-card-dashboard p-4">
<h5 class="mb-2">👤 <?= esc($a->patient_name) ?></h5>
<p class="mb-1 small text-muted">📅 <?= esc($a->appointment_date) ?></p>
<p class="mb-3 small text-muted"> <?= esc($a->appointment_time) ?></p>
<p class="mb-3">
<?php if (! empty($appointments)): ?>
<div class="row g-3">
<?php foreach ($appointments as $a): ?>
<?php
$st = trim((string) $a->status);
if ($st === '') {
@ -48,44 +72,68 @@
}
$badgeClass = match ($st) {
'pending' => 'bg-warning text-dark',
'approved' => 'bg-success',
'rejected' => 'bg-danger',
default => 'bg-secondary',
'pending' => 'ov-badge ov-badge--warning',
'approved' => 'ov-badge ov-badge--success',
'rejected' => 'ov-badge ov-badge--danger',
default => 'ov-badge',
};
?>
<span class="badge <?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
</p>
<div class="col-md-6 col-xl-4">
<div class="ov-panel h-100">
<div class="ov-panel__header">
<h2 class="ov-panel__title mb-0"><i class="bi bi-person me-1"></i><?= esc($a->patient_name) ?></h2>
<span class="<?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
</div>
<div class="ov-panel__body">
<p class="mb-1 text-muted small"><i class="bi bi-calendar-event me-1"></i><?= esc($a->appointment_date) ?></p>
<p class="mb-3 text-muted small"><i class="bi bi-clock me-1"></i><?= esc($a->appointment_time) ?></p>
<?php if ($st === 'pending'): ?>
<div class="d-flex gap-2 flex-wrap">
<form method="post" action="<?= base_url('doctor/appointment/' . $a->id . '/accept') ?>" class="d-inline">
<?= csrf_field() ?>
<button type="submit" class="btn btn-success btn-sm rounded-pill px-3">Accept</button>
</form>
<form method="post" action="<?= base_url('doctor/appointment/' . $a->id . '/reject') ?>" class="d-inline">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline-danger btn-sm rounded-pill px-3">Reject</button>
</form>
<?php if ($st === 'pending'): ?>
<div class="d-flex gap-2 flex-wrap">
<form method="post" action="<?= base_url('doctor/appointment/' . $a->id . '/accept') ?>" class="d-inline">
<?= csrf_field() ?>
<button type="submit" class="btn btn-success btn-sm rounded-pill px-3">Accept</button>
</form>
<form method="post" action="<?= base_url('doctor/appointment/' . $a->id . '/reject') ?>" class="d-inline">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline-danger btn-sm rounded-pill px-3">Reject</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($appointments)): ?>
<p class="text-center text-muted">No appointments yet.</p>
<?php endif; ?>
<div class="text-center mt-4">
<a href="<?= base_url('logout') ?>" class="btn btn-outline-danger btn-sm rounded-pill px-4">Logout</a>
</div>
<?php else: ?>
<div class="ov-panel">
<div class="ov-panel__body">
<p class="text-muted mb-0">No appointments yet.</p>
</div>
</div>
<?php endif; ?>
</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';
}
function toggleDropdown() {
document.getElementById('profileDropdown').classList.toggle('open');
}
document.addEventListener('click', function (e) {
const menu = document.getElementById('profileMenu');
if (!menu.contains(e.target)) {
document.getElementById('profileDropdown').classList.remove('open');
}
});
</script>
</body>
</html>

View File

@ -1,79 +1,136 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Doctor profile</title>
<title>Doctor Profile</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 href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
<link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>">
</head>
<body class="app-body app-page--doctor">
<body class="app-body overview-layout">
<?php $validationErrors = validation_errors(); ?>
<?php
$specializationOptions = $specializationOptions ?? [];
$selectedSpecializations = old('specialization', $selectedSpecializations ?? []);
<div class="container py-5" style="max-width: 560px;">
if (! is_array($selectedSpecializations)) {
$selectedSpecializations = $selectedSpecializations !== ''
? array_values(array_filter(array_map('trim', explode(',', (string) $selectedSpecializations))))
: [];
}
?>
<h2 class="text-center mb-4 app-heading">Your profile</h2>
<aside class="ov-sidebar" id="sidebar">
<div class="ov-brand"><h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1><span>Doctor Panel</span></div>
<nav class="ov-nav">
<div class="ov-nav__section">Doctor</div>
<a href="<?= base_url('doctor/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
<a href="<?= base_url('doctor/profile') ?>" class="ov-nav__link active"><i class="bi bi-person-gear"></i> Profile</a>
</nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert text-center"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<form method="post" action="<?= base_url('doctor/profile') ?>" class="app-form card app-card-dashboard p-4">
<?= csrf_field() ?>
<div class="mb-3">
<label class="form-label" for="specialization">Specialization</label>
<input type="text" name="specialization" id="specialization"
value="<?= esc(old('specialization', $doctor['specialization'] ?? '')) ?>"
class="form-control <?= isset($validationErrors['specialization']) ? 'is-invalid' : '' ?>"placeholder="e.g. Cardiology">
<?= validation_show_error('specialization') ?>
<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">Doctor Profile</p>
</div>
<a href="<?= base_url('doctor/dashboard') ?>" class="btn btn-sm btn-outline-secondary px-3">Back to dashboard</a>
</header>
<div class="mb-3">
<label class="form-label" for="experience">Experience</label>
<input type="text" name="experience" id="experience"
value="<?= esc(old('experience', $doctor['experience'] ?? '')) ?>"
class="form-control <?= isset($validationErrors['experience']) ? 'is-invalid' : '' ?>"placeholder="e.g. 10 years">
<?= validation_show_error('experience') ?>
</div>
<main class="ov-content">
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label" for="fees">Consultation fee</label>
<input type="text" name="fees" id="fees"
value="<?= esc(old('fees', $doctor['fees'] ?? '')) ?>"
class="form-control <?= isset($validationErrors['fees']) ? 'is-invalid' : '' ?>"
placeholder="e.g. 500.00">
<?= validation_show_error('fees') ?>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="available_from">Available from</label>
<input type="time" name="available_from" id="available_from"
value="<?= esc(old('available_from', $doctor['available_from'] ?? '')) ?>"
class="form-control <?= isset($validationErrors['available_from']) ? 'is-invalid' : '' ?>">
<?= validation_show_error('available_from') ?>
<div class="ov-panel" style="max-width: 720px;">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Update Profile</h2>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="available_to">Available to</label>
<input type="time" name="available_to" id="available_to"
value="<?= esc(old('available_to', $doctor['available_to'] ?? '')) ?>"
class="form-control <?= isset($validationErrors['available_to']) ? 'is-invalid' : '' ?>">
<?= validation_show_error('available_to') ?>
<div class="ov-panel__body">
<form method="post" action="<?= base_url('doctor/profile') ?>" class="app-form">
<?= csrf_field() ?>
<div class="mb-3">
<label class="form-label" for="specialization">Specialization</label>
<select name="specialization[]" id="specialization" class="form-select <?= isset($validationErrors['specialization']) ? 'is-invalid' : '' ?>" multiple required>
<?php foreach ($specializationOptions as $option): ?>
<option value="<?= esc($option) ?>" <?= in_array($option, $selectedSpecializations, true) ? 'selected' : '' ?>>
<?= esc($option) ?>
</option>
<?php endforeach; ?>
<?php foreach ($selectedSpecializations as $selected): ?>
<?php if (! in_array($selected, $specializationOptions, true)): ?>
<option value="<?= esc($selected) ?>" selected><?= esc($selected) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
<?= validation_show_error('specialization') ?>
</div>
<div class="mb-3">
<label class="form-label" for="experience">Experience</label>
<input type="text" name="experience" id="experience" value="<?= esc(old('experience', $doctor['experience'] ?? '')) ?>" class="form-control <?= isset($validationErrors['experience']) ? 'is-invalid' : '' ?>" placeholder="e.g. 10 years">
<?= validation_show_error('experience') ?>
</div>
<div class="mb-3">
<label class="form-label" for="fees">Consultation fee</label>
<input type="text" name="fees" id="fees" value="<?= esc(old('fees', $doctor['fees'] ?? '')) ?>" class="form-control <?= isset($validationErrors['fees']) ? 'is-invalid' : '' ?>" placeholder="e.g. 500.00">
<?= validation_show_error('fees') ?>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="available_from">Available from</label>
<input type="time" name="available_from" id="available_from" value="<?= esc(old('available_from', $doctor['available_from'] ?? '')) ?>" class="form-control <?= isset($validationErrors['available_from']) ? 'is-invalid' : '' ?>">
<?= validation_show_error('available_from') ?>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="available_to">Available to</label>
<input type="time" name="available_to" id="available_to" value="<?= esc(old('available_to', $doctor['available_to'] ?? '')) ?>" class="form-control <?= isset($validationErrors['available_to']) ? 'is-invalid' : '' ?>">
<?= validation_show_error('available_to') ?>
</div>
</div>
<div class="d-flex flex-wrap gap-2 justify-content-between mt-4">
<a href="<?= base_url('doctor/dashboard') ?>" class="btn btn-outline-secondary rounded-pill px-4">Back</a>
<button type="submit" class="btn btn-app-primary px-4">Save changes</button>
</div>
</form>
</div>
</div>
<div class="d-flex flex-wrap gap-2 justify-content-between mt-4">
<a href="<?= base_url('doctor/dashboard') ?>" class="btn btn-outline-secondary rounded-pill px-4">Back</a>
<button type="submit" class="btn btn-app-primary px-4">Save changes</button>
</div>
</form>
</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>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function () {
$('#specialization').select2({
placeholder: 'Select or type specializations',
tags: true,
width: '100%',
tokenSeparators: [',']
});
});
</script>
</body>
</html>

View File

@ -3,129 +3,147 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Book Appointment</title>
<title>Patient 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="<?= base_url('css/app.css') ?>">
<link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>">
</head>
<body class="app-body app-page--patient">
<body class="app-body overview-layout">
<?php $validationErrors = validation_errors(); ?>
<div class="container py-5">
<aside class="ov-sidebar" id="sidebar">
<div class="ov-brand"><h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1><span>Patient Panel</span></div>
<nav class="ov-nav">
<div class="ov-nav__section">Patient</div>
<a href="<?= base_url('patient/dashboard') ?>" class="ov-nav__link active"><i class="bi bi-calendar2-plus"></i> Book Appointment</a>
</nav>
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
</aside>
<h2 class="text-center mb-4 app-heading">Book appointment</h2>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert text-center"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (! empty($validationErrors)): ?>
<div class="alert alert-danger app-alert"><?= validation_list_errors() ?></div>
<?php endif; ?>
<?php if (! empty($myAppointments)): ?>
<div class="card app-card-dashboard p-4 mb-5">
<h3 class="h5 mb-3">My appointments</h3>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle mb-0 small">
<thead class="table-light">
<tr>
<th>Doctor</th>
<th>Date</th>
<th>Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($myAppointments as $ap): ?>
<tr>
<td>
<?= esc($ap->doctor_name) ?>
<?php if (! empty($ap->specialization)): ?>
<br><span class="text-muted"><?= esc($ap->specialization) ?></span>
<?php endif; ?>
</td>
<td><?= esc($ap->appointment_date) ?></td>
<td><?= esc($ap->appointment_time) ?></td>
<td>
<?php
$st = trim((string) $ap->status);
if ($st === '') {
$st = 'pending';
} elseif ($st === 'confirmed') {
$st = 'approved';
} elseif ($st === 'cancelled') {
$st = 'rejected';
}
$badgeClass = match ($st) {
'pending' => 'bg-warning text-dark',
'approved' => 'bg-success',
'rejected' => 'bg-danger',
default => 'bg-secondary',
};
?>
<span class="badge <?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<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">Book Appointment</p>
</div>
</div>
<?php endif; ?>
</header>
<h3 class="h5 text-center mb-3 text-muted">Available doctors</h3>
<main class="ov-content">
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (! empty($validationErrors)): ?>
<div class="alert alert-danger app-alert"><?= validation_list_errors() ?></div>
<?php endif; ?>
<div class="row">
<?php foreach ($doctors as $doc): ?>
<div class="col-md-4 mb-4">
<div class="card app-card-dashboard p-4 h-100">
<h5 class="mb-2">👨‍⚕️ <?= esc($doc->name) ?></h5>
<p class="text-muted small mb-3">Specialization: <?= esc($doc->specialization ?? 'N/A') ?></p>
<form method="post" action="<?= base_url('book-appointment') ?>" class="app-form" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="doctor_id" value="<?= esc($doc->doctor_id) ?>">
<div class="mb-2">
<label class="form-label small">Date</label>
<input type="date" name="date" value="<?= esc(old('date')) ?>"
class="form-control form-control-sm <?= ! empty($validationErrors['date']) ? 'is-invalid' : '' ?>"
required>
</div>
<div class="mb-3">
<label class="form-label small">Time</label>
<input type="time" name="time" value="<?= esc(old('time')) ?>"
class="form-control form-control-sm <?= ! empty($validationErrors['time']) ? 'is-invalid' : '' ?>"
required>
</div>
<button type="submit" class="btn btn-app-primary w-100 btn-sm">Book</button>
</form>
<?php if (! empty($myAppointments)): ?>
<div class="ov-panel mb-4">
<div class="ov-panel__header">
<h2 class="ov-panel__title">My Appointments</h2>
</div>
<div class="p-0">
<table class="table ov-mini-table mb-0">
<thead>
<tr>
<th class="ps-3">Doctor</th>
<th>Date</th>
<th>Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($myAppointments as $ap): ?>
<tr>
<td class="ps-3">
<?= esc($ap->doctor_name) ?>
<?php if (! empty($ap->specialization)): ?>
<br><span class="text-muted small"><?= esc($ap->specialization) ?></span>
<?php endif; ?>
</td>
<td><?= esc($ap->appointment_date) ?></td>
<td><?= esc($ap->appointment_time) ?></td>
<td>
<?php
$st = trim((string) $ap->status);
if ($st === '') {
$st = 'pending';
} elseif ($st === 'confirmed') {
$st = 'approved';
} elseif ($st === 'cancelled') {
$st = 'rejected';
}
$badgeClass = match ($st) {
'pending' => 'ov-badge ov-badge--warning',
'approved' => 'ov-badge ov-badge--success',
'rejected' => 'ov-badge ov-badge--danger',
default => 'ov-badge',
};
?>
<span class="<?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<div class="ov-panel mb-3">
<div class="ov-panel__header">
<h2 class="ov-panel__title">Available Doctors</h2>
</div>
<div class="ov-panel__body">
<?php if (! empty($doctors)): ?>
<div class="row g-3">
<?php foreach ($doctors as $doc): ?>
<div class="col-md-6 col-xl-4">
<div class="border rounded-3 p-3 h-100">
<h6 class="mb-1">Dr. <?= esc($doc->name) ?></h6>
<p class="text-muted small mb-3">Specialization: <?= esc($doc->specialization ?? 'N/A') ?></p>
<form method="post" action="<?= base_url('book-appointment') ?>" class="app-form" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="doctor_id" value="<?= esc($doc->doctor_id) ?>">
<div class="mb-2">
<label class="form-label small">Date</label>
<input type="date" name="date" value="<?= esc(old('date')) ?>" class="form-control form-control-sm <?= ! empty($validationErrors['date']) ? 'is-invalid' : '' ?>" required>
</div>
<div class="mb-3">
<label class="form-label small">Time</label>
<input type="time" name="time" value="<?= esc(old('time')) ?>" class="form-control form-control-sm <?= ! empty($validationErrors['time']) ? 'is-invalid' : '' ?>" required>
</div>
<button type="submit" class="btn btn-app-primary w-100 btn-sm">Book</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-muted mb-0">No doctors available yet.</p>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($doctors)): ?>
<p class="text-center text-muted">No doctors available yet.</p>
<?php endif; ?>
<div class="text-center mt-4">
<a href="<?= base_url('logout') ?>" class="btn btn-outline-danger btn-sm rounded-pill px-4">Logout</a>
</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>

44
public/css/add_doctor.css Normal file
View File

@ -0,0 +1,44 @@
.experience-unit {
width: 96px;
justify-content: center;
}
.experience-group .col-6:first-child .form-control {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.experience-group .col-6:first-child .input-group-text {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.experience-group .col-6:last-child .form-control {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.experience-group .col-6:last-child .input-group-text {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
margin-top: 0;
margin-bottom: 0;
padding: 0.1rem 0.45rem 0.1rem 1.05rem;
font-size: 0.90rem;
line-height: 1.15;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
left: 0rem;
top: 50%;
transform: translateY(-50%);
margin-right: 0;
}
.ov-content {
display: flex;
justify-content: center;
}

View File

@ -26,7 +26,7 @@ body.app-body {
align-items: center;
justify-content: center;
padding: 1.5rem;
background: linear-gradient(160deg, #e0f2fe 0%, #ccfbf1 40%, #f0fdfa 100%);
background: linear-gradient(160deg, #fff 0%, #fff 40%, #fff 100%);
}
.auth-card {
@ -114,6 +114,13 @@ a.btn-app-outline {
border-color: #dc3545;
}
/* Softer focus style to avoid strong blue outline */
.app-form .form-control:focus,
.app-form .form-select:focus {
border-color: #9ec5fe;
box-shadow: 0 0 0 0.12rem rgba(13, 110, 253, 0.12);
}
.app-page--patient {
min-height: 100vh;
padding-bottom: 2rem;
@ -229,3 +236,80 @@ a.btn-app-outline {
font-weight: 600;
font-size: 0.875rem;
}
.password-container {
position: relative;
width: 100%;
}
.password-container input {
width: 100%;
padding-right: 2.5rem;
}
.toggle-password {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #64748b;
line-height: 1;
}
.multi-select-arrow-wrap {
position: relative;
}
.select2-arrow-hint {
position: absolute;
right: 0.85rem;
top: 50%;
transform: translateY(-50%);
color: #64748b;
pointer-events: auto;
cursor: pointer;
z-index: 2;
}
/* Make Select2 single-line control visually match Bootstrap form controls */
.select2-container .select2-selection--multiple {
min-height: calc(1.5em + 0.75rem + 2px);
height: calc(1.5em + 0.75rem + 2px);
border: 1px solid #ced4da;
border-radius: 0.375rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
padding: 0.25rem 2rem 0.25rem 0.25rem;
overflow: hidden;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.select2-container .select2-selection--multiple .select2-selection__rendered {
font-size: 1rem;
font-weight: 400;
color: #495057;
line-height: 1.5;
padding: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select2-container .select2-selection--multiple .select2-selection__placeholder {
color: #6c757d;
font-weight: 400;
}
.select2-container .select2-search--inline .select2-search__field {
font-size: 15px;
font-weight: 400;
margin: 0;
line-height: 1.5;
}
.select2-container--default.select2-container--focus .select2-selection--multiple,
.select2-container--default .select2-selection--multiple:focus {
border-color: #9ec5fe;
box-shadow: 0 0 0 0.12rem rgba(13, 110, 253, 0.12);
}

479
public/css/dashboard.css Normal file
View File

@ -0,0 +1,479 @@
:root {
--sidebar-width: 230px;
--sidebar-bg: #1e293b;
--sidebar-text: #94a3b8;
--sidebar-text-active: #f1f5f9;
--topbar-bg: #ffffff;
--page-bg: #f8fafc;
--border: #e2e8f0;
--text: #1e293b;
--text-muted: #64748b;
}
* { box-sizing: border-box; }
body.overview-layout {
min-height: 100vh;
background: var(--page-bg);
display: flex;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
color: var(--text);
margin: 0;
overflow-x: hidden;
}
/* ── Sidebar ── */
.ov-sidebar {
width: var(--sidebar-width);
min-height: 100vh;
background: var(--sidebar-bg);
display: flex;
flex-direction: column;
position: fixed;
top: 0; left: 0; bottom: 0;
z-index: 100;
overflow-x: hidden;
}
.ov-brand {
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid rgba(255,255,255,0.07);
}
.ov-brand h1 {
font-size: 1rem;
font-weight: 700;
color: #f1f5f9;
margin: 0;
}
.ov-brand span {
font-size: 0.72rem;
color: var(--sidebar-text);
}
.ov-nav {
padding: 0.75rem 0;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.ov-nav__section {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #475569;
padding: 0.75rem 1.25rem 0.25rem;
}
.ov-nav__link {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.55rem 1.25rem;
color: var(--sidebar-text);
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
border-left: 2px solid transparent;
transition: background 0.12s, color 0.12s;
}
.ov-nav__link:hover {
background: rgba(255,255,255,0.05);
color: var(--sidebar-text-active);
}
.ov-nav__link.active {
background: rgba(255,255,255,0.08);
color: var(--sidebar-text-active);
border-left-color: #38bdf8;
}
.ov-nav__link i { font-size: 0.95rem; }
.ov-sidebar__footer {
padding: 1rem 1.25rem;
border-top: 1px solid rgba(255,255,255,0.07);
}
.ov-sidebar__footer a {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--sidebar-text);
font-size: 0.83rem;
text-decoration: none;
}
.ov-sidebar__footer a:hover { color: #f1f5f9; }
/* ── Main ── */
.ov-main {
margin-left: var(--sidebar-width);
flex: 1;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ── Topbar ── */
.ov-topbar {
background: var(--topbar-bg);
border-bottom: 1px solid var(--border);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 50;
}
.ov-topbar__title {
font-weight: 600;
font-size: 0.95rem;
color: var(--text);
margin: 0;
}
/* ── Profile / Avatar dropdown ── */
.ov-profile { position: relative; }
.ov-profile__btn {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 8px;
transition: background 0.12s;
}
.ov-profile__btn:hover { background: #f1f5f9; }
.ov-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: #334155;
display: flex;
align-items: center;
justify-content: center;
color: #f1f5f9;
font-size: 0.82rem;
font-weight: 700;
flex-shrink: 0;
}
.ov-profile__name {
font-size: 0.83rem;
font-weight: 600;
color: var(--text);
}
.ov-profile__caret {
font-size: 0.75rem;
color: var(--text-muted);
}
.ov-dropdown {
display: none;
position: absolute;
right: 0;
top: calc(100% + 6px);
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
min-width: 180px;
z-index: 200;
padding: 0.4rem 0;
}
.ov-dropdown.open { display: block; }
.ov-dropdown__header {
padding: 0.6rem 1rem 0.5rem;
border-bottom: 1px solid var(--border);
}
.ov-dropdown__name {
font-size: 0.85rem;
font-weight: 600;
color: var(--text);
margin: 0;
}
.ov-dropdown__role {
font-size: 0.72rem;
color: var(--text-muted);
}
.ov-dropdown__item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.83rem;
color: var(--text);
text-decoration: none;
transition: background 0.1s;
}
.ov-dropdown__item:hover { background: #f8fafc; color: var(--text); }
.ov-dropdown__item.danger { color: #dc2626; }
.ov-dropdown__item.danger:hover { background: #fef2f2; }
.ov-dropdown__divider {
border: none;
border-top: 1px solid var(--border);
margin: 0.3rem 0;
}
/* ── Content ── */
.ov-content { padding: 1.5rem; flex: 1; }
/* ── Stat cards ── */
.ov-stat {
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.25rem;
display: flex;
align-items: center;
gap: 1rem;
}
.ov-stat__icon {
width: 44px;
height: 44px;
border-radius: 8px;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: #475569;
flex-shrink: 0;
}
.ov-stat__label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
margin-bottom: 0.1rem;
}
.ov-stat__value {
font-size: 1.75rem;
font-weight: 700;
color: var(--text);
line-height: 1;
margin: 0;
}
/* ── Panels ── */
.ov-panel {
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
}
.ov-panel__header {
padding: 0.85rem 1.25rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.ov-panel__title {
font-weight: 600;
font-size: 0.88rem;
color: var(--text);
margin: 0;
}
.ov-panel__body { padding: 1rem 1.25rem; }
/* ── Quick actions ── */
.ov-action {
display: block;
text-align: center;
padding: 0.9rem 0.5rem;
border: 1px solid var(--border);
border-radius: 8px;
background: #f8fafc;
color: var(--text);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
transition: background 0.12s, border-color 0.12s;
}
.ov-action:hover {
background: #f1f5f9;
border-color: #cbd5e1;
color: var(--text);
}
.ov-action i {
display: block;
font-size: 1.35rem;
margin-bottom: 0.35rem;
color: #475569;
}
/* ── Activity ── */
.ov-activity-item {
display: flex;
align-items: flex-start;
gap: 0.65rem;
padding: 0.6rem 0;
border-bottom: 1px solid #f1f5f9;
}
.ov-activity-item:last-child { border-bottom: none; }
.ov-activity-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: 0.35rem;
flex-shrink: 0;
}
.ov-activity-text {
font-size: 0.82rem;
color: var(--text);
}
.ov-activity-time {
font-size: 0.7rem;
color: var(--text-muted);
margin-top: 0.1rem;
}
/* ── Mini table ── */
.ov-mini-table th {
background: #f8fafc;
color: var(--text-muted);
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
border-color: var(--border);
}
.ov-mini-table td {
font-size: 0.83rem;
color: var(--text);
border-color: #f1f5f9;
vertical-align: middle;
}
/* ── Badges ── */
.ov-badge {
font-size: 0.7rem;
font-weight: 600;
border-radius: 999px;
padding: 0.18em 0.6em;
}
.ov-badge--success { background: #dcfce7; color: #15803d; }
.ov-badge--warning { background: #fef9c3; color: #92400e; }
.ov-badge--danger { background: #fee2e2; color: #b91c1c; }
/* ── Sidebar Toggle ── */
.ov-sidebar {
transition: transform 0.25s ease;
}
.ov-sidebar.collapsed {
transform: translateX(-100%);
}
.ov-main {
transition: margin-left 0.25s ease;
}
.ov-main.expanded {
margin-left: 0;
}
.ov-toggle-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.3rem 0.5rem;
border-radius: 6px;
color: var(--text-muted);
font-size: 1.2rem;
line-height: 1;
transition: background 0.12s;
margin-right: 0.75rem;
}
.ov-toggle-btn:hover {
background: #f1f5f9;
color: var(--text);
}
/* ── Responsive ── */
@media (max-width: 768px) {
.ov-sidebar { display: none; }
.ov-main { margin-left: 0; }
}
/* Hide dropdown initially */
.ov-dropdown-menu {
display: none;
padding-left: 30px;
}
/* Show when active */
.ov-nav__dropdown.active .ov-dropdown-menu {
display: block;
}
/* Sub-links */
.ov-nav__sublink {
display: block;
padding: 8px 0;
font-size: 14px;
color: #aaa;
text-decoration: none;
transition: 0.3s;
}
.ov-nav__sublink:hover {
color: #fff;
transform: translateX(5px);
}
/* Dropdown icon style */
.dropdown-icon {
font-size: 12px;
color: inherit;
flex-shrink: 0;
transition: transform 0.3s ease;
}
/* Rotate when open */
.ov-nav__dropdown.active .dropdown-icon {
transform: rotate(180deg);
}
.ov-nav__link {
transition: 0.3s;
}
.ov-nav__link:hover {
background: rgba(255,255,255,0.05);
border-radius: 8px;
}

89
public/css/doctors.css Normal file
View File

@ -0,0 +1,89 @@
.dataTables_wrapper {
padding: 1rem;
}
.dataTables_filter input {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 0.375rem 0.75rem;
}
#doctorsTable thead th,
#patientsTable thead th,
#dashboardDoctorsTable thead th,
#dashboardPatientsTable thead th {
white-space: nowrap;
}
#doctorsTable.table.dataTable thead .sorting,
#doctorsTable.table.dataTable thead .sorting_asc,
#doctorsTable.table.dataTable thead .sorting_desc,
#doctorsTable.table.dataTable thead .sorting_asc_disabled,
#doctorsTable.table.dataTable thead .sorting_desc_disabled,
#patientsTable.table.dataTable thead .sorting,
#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,
#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;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.btn-group .btn {
border-radius: 0;
}
.btn-group .btn:first-child {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
}
.btn-group .btn:last-child {
border-top-right-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
.badge {
font-size: 0.75rem;
}
.sort-indicator {
font-size: 0.85rem;
opacity: 0.55;
}
.sort-indicator--active {
opacity: 0.9;
color: #0d6efd;
}
.specialization-toggle {
cursor: pointer;
}
.sort-icon-link {
display: inline-flex;
align-items: center;
margin-left: 0.2rem;
color: inherit;
text-decoration: none;
}
.sort-icon-link:hover {
color: inherit;
text-decoration: none;
}

14
public/js/script.js Normal file
View File

@ -0,0 +1,14 @@
// 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");
// }
// }