Compare commits
No commits in common. "main" and "feature/doctor-dashboard" have entirely different histories.
main
...
feature/do
@ -88,5 +88,5 @@ class Autoload extends AutoloadConfig
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public $helpers = ['form', 'url', 'encryption'];
|
||||
public $helpers = ['form', 'url'];
|
||||
}
|
||||
|
||||
@ -16,13 +16,7 @@ $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');
|
||||
@ -43,7 +37,3 @@ $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');
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ActivityLogModel;
|
||||
|
||||
class ActivityLog extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if ($r = $this->requireRole('admin')) {
|
||||
return $r;
|
||||
}
|
||||
|
||||
$logModel = new ActivityLogModel();
|
||||
$filters = [
|
||||
'action' => trim((string) $this->request->getGet('action')),
|
||||
'role' => trim((string) $this->request->getGet('role')),
|
||||
'date_from' => trim((string) $this->request->getGet('date_from')),
|
||||
'date_to' => trim((string) $this->request->getGet('date_to')),
|
||||
];
|
||||
|
||||
return view('admin/activity_log', [
|
||||
'logs' => $logModel->getFiltered($filters),
|
||||
'filters' => $filters,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -2,160 +2,13 @@
|
||||
|
||||
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')) {
|
||||
@ -165,30 +18,31 @@ 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;
|
||||
}
|
||||
|
||||
return view('admin/doctors');
|
||||
$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);
|
||||
}
|
||||
|
||||
public function deleteDoctor($id)
|
||||
@ -204,13 +58,11 @@ class Admin extends BaseController
|
||||
|
||||
$userModel = new UserModel();
|
||||
$doctorModel = new DoctorModel();
|
||||
$appointmentModel = new AppointmentModel();
|
||||
$doctorSpecializationModel = new DoctorSpecializationModel();
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$doctor = $doctorModel->findByUserId($id);
|
||||
$doctor = $doctorModel->where('user_id', $id)->first();
|
||||
if ($doctor) {
|
||||
$appointmentModel->deleteByDoctorId((int) $doctor['id']);
|
||||
$doctorSpecializationModel->deleteByDoctorId((int) $doctor['id']);
|
||||
$db->table('appointments')->where('doctor_id', $doctor['id'])->delete();
|
||||
$doctorModel->delete($doctor['id']);
|
||||
}
|
||||
|
||||
@ -225,7 +77,18 @@ class Admin extends BaseController
|
||||
return $r;
|
||||
}
|
||||
|
||||
return view('admin/patients');
|
||||
$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);
|
||||
}
|
||||
|
||||
public function deletePatient($id)
|
||||
@ -241,11 +104,11 @@ class Admin extends BaseController
|
||||
|
||||
$userModel = new UserModel();
|
||||
$patientModel = new PatientModel();
|
||||
$appointmentModel = new AppointmentModel();
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$patient = $patientModel->findByUserId($id);
|
||||
$patient = $patientModel->where('user_id', $id)->first();
|
||||
if ($patient) {
|
||||
$appointmentModel->deleteByPatientId((int) $patient['id']);
|
||||
$db->table('appointments')->where('patient_id', $patient['id'])->delete();
|
||||
$patientModel->delete($patient['id']);
|
||||
}
|
||||
|
||||
@ -260,8 +123,18 @@ class Admin extends BaseController
|
||||
return $r;
|
||||
}
|
||||
|
||||
$appointmentModel = new AppointmentModel();
|
||||
$data['appointments'] = $appointmentModel->getAdminAppointments();
|
||||
$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();
|
||||
|
||||
return view('admin/appointments', $data);
|
||||
}
|
||||
@ -272,63 +145,7 @@ class Admin extends BaseController
|
||||
return $r;
|
||||
}
|
||||
|
||||
$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,
|
||||
]));
|
||||
return view('admin/add_doctor');
|
||||
}
|
||||
|
||||
public function storeDoctor()
|
||||
@ -337,55 +154,86 @@ class Admin extends BaseController
|
||||
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]',
|
||||
'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',
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
$userModel = new UserModel();
|
||||
$doctorModel = new DoctorModel();
|
||||
$specializationModel = new SpecializationModel();
|
||||
$doctorSpecializationModel = new DoctorSpecializationModel();
|
||||
$db = \Config\Database::connect();
|
||||
$validation = \Config\Services::validation();
|
||||
|
||||
$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.');
|
||||
$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'),
|
||||
]];
|
||||
}
|
||||
|
||||
$specializationValue = implode(', ', $specializations);
|
||||
if (strlen($specializationValue) > 191) {
|
||||
return redirect()->back()->withInput()->with('error', 'Selected specializations are too long. Please reduce them.');
|
||||
$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]',
|
||||
];
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$yearsRaw = (string) $this->request->getPost('experience_years');
|
||||
$monthsRaw = (string) $this->request->getPost('experience_months');
|
||||
$years = $yearsRaw === '' ? null : (int) $yearsRaw;
|
||||
$months = $monthsRaw === '' ? null : (int) $monthsRaw;
|
||||
$emailKey = strtolower($row['email']);
|
||||
if (isset($emailsSeen[$emailKey])) {
|
||||
$error = 'Row ' . $rowNumber . ': Duplicate email in submitted rows.';
|
||||
break;
|
||||
}
|
||||
$emailsSeen[$emailKey] = true;
|
||||
|
||||
$experience = $this->buildExperienceString($years, $months);
|
||||
$generatedPassword = $this->generateAccountPassword();
|
||||
if ($userModel->where('email', $row['email'])->first()) {
|
||||
$error = 'Row ' . $rowNumber . ': Email already exists.';
|
||||
break;
|
||||
}
|
||||
|
||||
$cleanRows[] = $row;
|
||||
}
|
||||
|
||||
if ($error !== null || $cleanRows === []) {
|
||||
return redirect()->back()->withInput()->with('error', $error ?? 'Please provide at least one doctor row.');
|
||||
}
|
||||
|
||||
$db->transStart();
|
||||
|
||||
foreach ($cleanRows as $row) {
|
||||
$userData = [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'email' => trim((string) $this->request->getPost('email')),
|
||||
'password' => password_hash($generatedPassword, PASSWORD_DEFAULT),
|
||||
'name' => $row['name'],
|
||||
'email' => $row['email'],
|
||||
'password' => password_hash($row['password'], PASSWORD_DEFAULT),
|
||||
'role' => 'doctor',
|
||||
'status' => 'active',
|
||||
];
|
||||
@ -393,105 +241,25 @@ class Admin extends BaseController
|
||||
if (! $userModel->skipValidation(true)->insert($userData)) {
|
||||
$db->transRollback();
|
||||
|
||||
return redirect()->back()->withInput()->with('error', 'Could not create doctor login account.');
|
||||
return redirect()->back()->withInput()->with('error', 'Could not create user for ' . $row['email'] . '.');
|
||||
}
|
||||
|
||||
$userId = (int) $userModel->getInsertID();
|
||||
|
||||
$doctorRow = [
|
||||
'user_id' => $userId,
|
||||
'specialization' => $specializationValue,
|
||||
'experience' => $experience,
|
||||
'fees' => $this->request->getPost('fees') !== '' && $this->request->getPost('fees') !== null
|
||||
? $this->request->getPost('fees')
|
||||
: null,
|
||||
'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,
|
||||
];
|
||||
|
||||
if (! $doctorModel->skipValidation(true)->insert($doctorRow)) {
|
||||
$db->transRollback();
|
||||
|
||||
return redirect()->back()->withInput()->with('error', 'Could not create doctor profile.');
|
||||
return redirect()->back()->withInput()->with('error', 'Could not create doctor profile for ' . $row['email'] . '.');
|
||||
}
|
||||
|
||||
$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();
|
||||
@ -500,232 +268,8 @@ class Admin extends BaseController
|
||||
return redirect()->back()->withInput()->with('error', 'Transaction failed.');
|
||||
}
|
||||
|
||||
$logModel = new ActivityLogModel();
|
||||
$logModel->log('create_patient', "Admin created patient {$firstName} {$lastName}", 'user', $userId);
|
||||
$created = count($cleanRows);
|
||||
|
||||
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,
|
||||
]);
|
||||
return redirect()->to(site_url('admin/doctors'))->with('success', $created . ' doctor account(s) created.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ActivityLogModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\PatientModel;
|
||||
|
||||
@ -21,11 +20,10 @@ class Auth extends BaseController
|
||||
public function registerProcess()
|
||||
{
|
||||
$rules = [
|
||||
'first_name' => 'required|min_length[2]|max_length[50]|alpha_space',
|
||||
'last_name' => 'required|min_length[2]|max_length[50]|alpha_space',
|
||||
'name' => 'required|min_length[3]|max_length[100]|alpha_numeric_punct',
|
||||
'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]).+$/]',
|
||||
'phone' => 'required|min_length[10]|max_length[10]',
|
||||
'password' => 'required|min_length[8]',
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
@ -33,12 +31,9 @@ class Auth extends BaseController
|
||||
}
|
||||
|
||||
$userModel = new UserModel();
|
||||
$firstName = trim((string) $this->request->getPost('first_name'));
|
||||
$lastName = trim((string) $this->request->getPost('last_name'));
|
||||
|
||||
$data = [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'name' => $this->request->getPost('name'),
|
||||
'email' => $this->request->getPost('email'),
|
||||
'password' => password_hash((string) $this->request->getPost('password'), PASSWORD_DEFAULT),
|
||||
'role' => 'patient',
|
||||
@ -54,12 +49,9 @@ class Auth extends BaseController
|
||||
$patientModel = new PatientModel();
|
||||
$patientModel->insert([
|
||||
'user_id' => $user_id,
|
||||
'phone' => '+91' . $this->request->getPost('phone'),
|
||||
'phone' => $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.');
|
||||
}
|
||||
|
||||
@ -95,9 +87,6 @@ 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'));
|
||||
}
|
||||
@ -114,14 +103,8 @@ 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')
|
||||
@ -183,7 +166,7 @@ class Auth extends BaseController
|
||||
{
|
||||
$rules = [
|
||||
'token' => 'required',
|
||||
'password' => 'required|min_length[8]|regex_match[/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z\d]).+$/]',
|
||||
'password' => 'required|min_length[8]',
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
|
||||
@ -3,54 +3,12 @@
|
||||
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')) {
|
||||
@ -69,8 +27,7 @@ class Doctor extends BaseController
|
||||
$doctorId = (int) $doctor['id'];
|
||||
|
||||
$query = $db->query('
|
||||
SELECT a.*,
|
||||
TRIM(CONCAT(COALESCE(u.first_name, \'\'), \' \', COALESCE(u.last_name, \'\'))) AS patient_name
|
||||
SELECT a.*, u.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
|
||||
@ -89,7 +46,6 @@ class Doctor extends BaseController
|
||||
}
|
||||
|
||||
$doctorModel = new DoctorModel();
|
||||
$specializationModel = new SpecializationModel();
|
||||
$userId = (int) session()->get('id');
|
||||
$doctor = $doctorModel->where('user_id', $userId)->first();
|
||||
|
||||
@ -99,53 +55,35 @@ class Doctor extends BaseController
|
||||
|
||||
if ($this->request->is('post')) {
|
||||
$rules = [
|
||||
'specialization' => 'required',
|
||||
'specialization' => 'required|min_length[2]|max_length[191]',
|
||||
'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' => implode(', ', $specializations),
|
||||
'specialization' => $this->request->getPost('specialization'),
|
||||
'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.');
|
||||
}
|
||||
|
||||
$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,
|
||||
]);
|
||||
return view('doctor/profile', ['doctor' => $doctor]);
|
||||
}
|
||||
|
||||
public function accept($id): ResponseInterface
|
||||
@ -197,9 +135,6 @@ class Doctor extends BaseController
|
||||
$status = AppointmentModel::normalizeStatus($status);
|
||||
$appointmentModel->update($appointmentId, ['status' => $status]);
|
||||
|
||||
$logModel = new ActivityLogModel();
|
||||
$logModel->log('update_appointment_status', "Doctor changed appointment status to {$status}", 'appointment', $appointmentId);
|
||||
|
||||
return redirect()->back()->with('success', 'Appointment updated.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\AppointmentModel;
|
||||
use App\Models\ActivityLogModel;
|
||||
use App\Models\PatientModel;
|
||||
|
||||
class Patient extends BaseController
|
||||
@ -24,9 +23,7 @@ class Patient extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$query = $db->query("
|
||||
SELECT doctors.id AS doctor_id,
|
||||
TRIM(CONCAT(COALESCE(users.first_name, ''), ' ', COALESCE(users.last_name, ''))) AS name,
|
||||
doctors.specialization
|
||||
SELECT doctors.id AS doctor_id, users.name, doctors.specialization
|
||||
FROM users
|
||||
JOIN doctors ON doctors.user_id = users.id
|
||||
WHERE users.role = 'doctor'
|
||||
@ -41,8 +38,7 @@ class Patient extends BaseController
|
||||
if ($patient) {
|
||||
$data['myAppointments'] = $db->query('
|
||||
SELECT a.id, a.appointment_date, a.appointment_time, a.status,
|
||||
TRIM(CONCAT(COALESCE(u.first_name, \'\'), \' \', COALESCE(u.last_name, \'\'))) AS doctor_name,
|
||||
doctors.specialization
|
||||
u.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
|
||||
@ -104,10 +100,6 @@ 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.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,7 @@ class InitAppointmentSchema extends Migration
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'first_name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'last_name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 100],
|
||||
'email' => ['type' => 'VARCHAR', 'constraint' => 191],
|
||||
'password' => ['type' => 'VARCHAR', 'constraint' => 255],
|
||||
'role' => ['type' => 'VARCHAR', 'constraint' => 20],
|
||||
@ -40,7 +39,7 @@ class InitAppointmentSchema extends Migration
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'user_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'dob' => ['type' => 'DATE', 'null' => true],
|
||||
'age' => ['type' => 'INT', 'null' => true],
|
||||
'gender' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true],
|
||||
'phone' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true],
|
||||
]);
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
<?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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
<?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'
|
||||
");
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<?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
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@ -67,44 +67,6 @@ 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 = [];
|
||||
|
||||
@ -12,7 +12,7 @@ class DoctorModel extends Model
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['user_id','specialization','experience','fees'];
|
||||
protected $allowedFields = ['user_id','specialization','experience','fees','available_from','available_to'];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
@ -43,44 +43,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ class PatientModel extends Model
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['user_id','dob','gender','phone'];
|
||||
protected $allowedFields = ['user_id','age','gender','phone'];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
@ -43,42 +43,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@ -13,9 +13,7 @@ class UserModel extends Model
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'formatted_user_id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
@ -47,61 +45,11 @@ class UserModel extends Model
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = ['assignFormattedUserId'];
|
||||
protected $afterInsert = [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
<!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>
|
||||
@ -3,259 +3,147 @@
|
||||
<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 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] : [];
|
||||
<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' => '',
|
||||
]];
|
||||
}
|
||||
?>
|
||||
<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 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>
|
||||
<div class="container py-5" style="max-width: 980px;">
|
||||
|
||||
<h2 class="text-center mb-4 app-heading">Add Doctors</h2>
|
||||
|
||||
<main class="ov-content">
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<div class="alert alert-danger app-alert text-center"><?= 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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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="row g-3">
|
||||
<div class="col-md-6">
|
||||
<?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'
|
||||
]);
|
||||
?>
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<?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'
|
||||
]);
|
||||
?>
|
||||
<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">
|
||||
</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 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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-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-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 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 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>
|
||||
</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';
|
||||
}
|
||||
(() => {
|
||||
const rowsContainer = document.getElementById('doctor-rows');
|
||||
const addBtn = document.getElementById('add-doctor-row');
|
||||
|
||||
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;
|
||||
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';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$specialization.select2({
|
||||
placeholder: 'Select or type specializations',
|
||||
tags: true,
|
||||
width: '100%',
|
||||
tokenSeparators: [',']
|
||||
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();
|
||||
});
|
||||
|
||||
$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');
|
||||
rowsContainer.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement) || !target.classList.contains('remove-row')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$specialization.select2('open');
|
||||
const row = target.closest('[data-row]');
|
||||
if (!row) return;
|
||||
row.remove();
|
||||
updateRowNumbers();
|
||||
});
|
||||
});
|
||||
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));
|
||||
}
|
||||
updateRowNumbers();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,211 +0,0 @@
|
||||
<!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>
|
||||
@ -5,62 +5,20 @@
|
||||
<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 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>
|
||||
<body class="app-body app-page--admin">
|
||||
|
||||
<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>
|
||||
<div class="container py-5">
|
||||
|
||||
<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">
|
||||
<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 class="ps-3">Sl No</th>
|
||||
<th>Sl No</th>
|
||||
<th>Patient</th>
|
||||
<th>Doctor</th>
|
||||
<th>Date</th>
|
||||
@ -68,40 +26,29 @@
|
||||
<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><?= $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>
|
||||
</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>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!-- <!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@ -54,376 +54,5 @@
|
||||
|
||||
</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']) ?> · <?= 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>
|
||||
|
||||
@ -5,356 +5,64 @@
|
||||
<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 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 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>
|
||||
<body class="app-body app-page--admin">
|
||||
|
||||
<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>
|
||||
<div class="container py-5">
|
||||
|
||||
<h2 class="text-center mb-4 app-heading">Doctors</h2>
|
||||
|
||||
<main class="ov-content">
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<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"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<div class="alert alert-danger app-alert text-center"><?= 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 class="text-center mb-4">
|
||||
<a href="<?= base_url('admin/doctors/add') ?>" class="btn btn-app-primary">Add doctor</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<table id="doctorsTable" class="table table-hover" style="width:100%">
|
||||
<thead class="table-light">
|
||||
|
||||
<div class="app-table-wrap">
|
||||
|
||||
<table class="table table-bordered table-hover text-center align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="60">User ID</th>
|
||||
<th>Doctor Name</th>
|
||||
<th>Email</th>
|
||||
<th>Sl No</th>
|
||||
<th>Name</th>
|
||||
<th>Specialization</th>
|
||||
<th>Experience</th>
|
||||
<th>Consultation Fee</th>
|
||||
<th width="120">Status</th>
|
||||
<th width="130">Actions</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>
|
||||
|
||||
</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>
|
||||
|
||||
</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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@ -5,276 +5,53 @@
|
||||
<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 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>
|
||||
<body class="app-body app-page--admin">
|
||||
|
||||
<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>
|
||||
<div class="container py-5">
|
||||
|
||||
<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; ?>
|
||||
<h2 class="text-center mb-4 app-heading">Patients</h2>
|
||||
|
||||
<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">
|
||||
<div class="app-table-wrap">
|
||||
|
||||
<table class="table table-bordered table-hover text-center align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="60">User ID</th>
|
||||
<th>Patient Name</th>
|
||||
<th>Email</th>
|
||||
<th>Sl No</th>
|
||||
<th>Name</th>
|
||||
<th>Phone</th>
|
||||
<th width="120">Status</th>
|
||||
<th width="130">Actions</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>
|
||||
|
||||
</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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
<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">
|
||||
|
||||
@ -36,13 +35,9 @@
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<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>
|
||||
placeholder="Enter password" autocomplete="current-password" required>
|
||||
<?= validation_show_error('password') ?>
|
||||
</div>
|
||||
|
||||
@ -59,6 +54,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="<?= base_url('js/script.js') ?>"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -5,10 +5,7 @@
|
||||
<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">
|
||||
|
||||
@ -26,35 +23,15 @@
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<?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'
|
||||
]);
|
||||
?>
|
||||
<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') ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<?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>
|
||||
<label for="email" class="form-label">Email</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>
|
||||
@ -62,27 +39,23 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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')) ?>"
|
||||
<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="Enter phone number"
|
||||
autocomplete="tel"
|
||||
required>
|
||||
</div>
|
||||
placeholder="Phone number" autocomplete="tel" required>
|
||||
<?= 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>
|
||||
<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>
|
||||
|
||||
<p class="small text-muted mb-4">Register as a <strong>patient</strong> to book appointments. Doctor accounts are created by an administrator.</p>
|
||||
|
||||
<button type="submit" class="btn btn-app-primary w-100">Register</button>
|
||||
</form>
|
||||
@ -93,5 +66,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -27,26 +27,13 @@
|
||||
|
||||
<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 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>
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Reset Password</button>
|
||||
</form>
|
||||
|
||||
@ -57,67 +44,6 @@
|
||||
</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>
|
||||
@ -1,181 +0,0 @@
|
||||
<?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>
|
||||
@ -1,128 +0,0 @@
|
||||
<?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>
|
||||
@ -5,62 +5,38 @@
|
||||
<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 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>
|
||||
<body class="app-body app-page--doctor">
|
||||
|
||||
<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>
|
||||
<div class="container py-5">
|
||||
|
||||
<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>
|
||||
<h2 class="text-center mb-4 app-heading">Doctor dashboard</h2>
|
||||
|
||||
<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>
|
||||
<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"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (! empty($appointments)): ?>
|
||||
<div class="row g-3">
|
||||
<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
|
||||
$st = trim((string) $a->status);
|
||||
if ($st === '') {
|
||||
@ -72,21 +48,14 @@
|
||||
}
|
||||
|
||||
$badgeClass = match ($st) {
|
||||
'pending' => 'ov-badge ov-badge--warning',
|
||||
'approved' => 'ov-badge ov-badge--success',
|
||||
'rejected' => 'ov-badge ov-badge--danger',
|
||||
default => 'ov-badge',
|
||||
'pending' => 'bg-warning text-dark',
|
||||
'approved' => 'bg-success',
|
||||
'rejected' => 'bg-danger',
|
||||
default => 'bg-secondary',
|
||||
};
|
||||
?>
|
||||
<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>
|
||||
<span class="badge <?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
|
||||
</p>
|
||||
|
||||
<?php if ($st === 'pending'): ?>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
@ -100,40 +69,23 @@
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</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 if (empty($appointments)): ?>
|
||||
<p class="text-center text-muted">No appointments yet.</p>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<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>
|
||||
|
||||
</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>
|
||||
@ -1,101 +1,68 @@
|
||||
<!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 overview-layout">
|
||||
<body class="app-body app-page--doctor">
|
||||
|
||||
<?php $validationErrors = validation_errors(); ?>
|
||||
<?php
|
||||
$specializationOptions = $specializationOptions ?? [];
|
||||
$selectedSpecializations = old('specialization', $selectedSpecializations ?? []);
|
||||
|
||||
if (! is_array($selectedSpecializations)) {
|
||||
$selectedSpecializations = $selectedSpecializations !== ''
|
||||
? array_values(array_filter(array_map('trim', explode(',', (string) $selectedSpecializations))))
|
||||
: [];
|
||||
}
|
||||
?>
|
||||
<div class="container py-5" style="max-width: 560px;">
|
||||
|
||||
<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>
|
||||
<h2 class="text-center mb-4 app-heading">Your profile</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<main class="ov-content">
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<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"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ov-panel" style="max-width: 720px;">
|
||||
<div class="ov-panel__header">
|
||||
<h2 class="ov-panel__title">Update Profile</h2>
|
||||
</div>
|
||||
<div class="ov-panel__body">
|
||||
<form method="post" action="<?= base_url('doctor/profile') ?>" class="app-form">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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' : '' ?>">
|
||||
<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' : '' ?>">
|
||||
<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>
|
||||
@ -105,32 +72,8 @@ if (! is_array($selectedSpecializations)) {
|
||||
<button type="submit" class="btn btn-app-primary px-4">Save changes</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';
|
||||
}
|
||||
</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>
|
||||
@ -3,53 +3,36 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Patient Dashboard</title>
|
||||
<title>Book Appointment</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">
|
||||
<body class="app-body app-page--patient">
|
||||
|
||||
<?php $validationErrors = validation_errors(); ?>
|
||||
|
||||
<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>
|
||||
<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">Book Appointment</p>
|
||||
</div>
|
||||
</header>
|
||||
<h2 class="text-center mb-4 app-heading">Book appointment</h2>
|
||||
|
||||
<main class="ov-content">
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success app-alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<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"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<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="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>
|
||||
<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 class="ps-3">Doctor</th>
|
||||
<th>Doctor</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Status</th>
|
||||
@ -58,10 +41,10 @@
|
||||
<tbody>
|
||||
<?php foreach ($myAppointments as $ap): ?>
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<td>
|
||||
<?= esc($ap->doctor_name) ?>
|
||||
<?php if (! empty($ap->specialization)): ?>
|
||||
<br><span class="text-muted small"><?= esc($ap->specialization) ?></span>
|
||||
<br><span class="text-muted"><?= esc($ap->specialization) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc($ap->appointment_date) ?></td>
|
||||
@ -78,13 +61,13 @@
|
||||
}
|
||||
|
||||
$badgeClass = match ($st) {
|
||||
'pending' => 'ov-badge ov-badge--warning',
|
||||
'approved' => 'ov-badge ov-badge--success',
|
||||
'rejected' => 'ov-badge ov-badge--danger',
|
||||
default => 'ov-badge',
|
||||
'pending' => 'bg-warning text-dark',
|
||||
'approved' => 'bg-success',
|
||||
'rejected' => 'bg-danger',
|
||||
default => 'bg-secondary',
|
||||
};
|
||||
?>
|
||||
<span class="<?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
|
||||
<span class="badge <?= $badgeClass ?>"><?= esc(ucfirst($st)) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@ -94,17 +77,16 @@
|
||||
</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">
|
||||
<h3 class="h5 text-center mb-3 text-muted">Available doctors</h3>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<?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>
|
||||
<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>
|
||||
@ -113,37 +95,37 @@
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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 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>
|
||||
</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>
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@ -26,7 +26,7 @@ body.app-body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(160deg, #fff 0%, #fff 40%, #fff 100%);
|
||||
background: linear-gradient(160deg, #e0f2fe 0%, #ccfbf1 40%, #f0fdfa 100%);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
@ -114,13 +114,6 @@ 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;
|
||||
@ -236,80 +229,3 @@ 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);
|
||||
}
|
||||
|
||||
@ -1,479 +0,0 @@
|
||||
: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;
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// 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");
|
||||
// }
|
||||
// }
|
||||
Loading…
x
Reference in New Issue
Block a user