updated_features
This commit is contained in:
parent
3760875a3d
commit
e38122209c
70
.env.example
Normal file
70
.env.example
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#--------------------------------------------------------------------
|
||||||
|
# Example Environment Configuration file
|
||||||
|
#
|
||||||
|
# This file can be used as a starting point for your own
|
||||||
|
# custom .env files, and contains most of the possible settings
|
||||||
|
# available in a default install.
|
||||||
|
#
|
||||||
|
# By default, all of the settings are commented out. If you want
|
||||||
|
# to override the setting, you must un-comment it by removing the '#'
|
||||||
|
# at the beginning of the line.
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# ENVIRONMENT
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
CI_ENVIRONMENT = development
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# APP
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Set this to your public URL (trailing slash). Example for XAMPP:
|
||||||
|
app.baseURL = 'http://localhost:8080/'
|
||||||
|
# If you have trouble with `.`, you could also use `_`.
|
||||||
|
# app_baseURL = ''
|
||||||
|
# app.forceGlobalSecureRequests = false
|
||||||
|
# app.CSPEnabled = false
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# DATABASE
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
database.default.hostname = localhost
|
||||||
|
database.default.database =
|
||||||
|
database.default.username = root
|
||||||
|
database.default.password =
|
||||||
|
database.default.DBDriver = MySQLi
|
||||||
|
database.default.DBPrefix =
|
||||||
|
database.default.port = 3306
|
||||||
|
|
||||||
|
# If you use MySQLi as tests, first update the values of Config\Database::$tests.
|
||||||
|
# database.tests.hostname = localhost
|
||||||
|
# database.tests.database = ci4_test
|
||||||
|
# database.tests.username = root
|
||||||
|
# database.tests.password = root
|
||||||
|
# database.tests.DBDriver = MySQLi
|
||||||
|
# database.tests.DBPrefix =
|
||||||
|
# database.tests.charset = utf8mb4
|
||||||
|
# database.tests.DBCollat = utf8mb4_general_ci
|
||||||
|
# database.tests.port = 3306
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# ENCRYPTION
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
encryption.key =
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# SESSION
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
|
||||||
|
# session.savePath = null
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# LOGGER
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
# logger.threshold = 4
|
||||||
@ -40,7 +40,7 @@ class App extends BaseConfig
|
|||||||
* something else. If you have configured your web server to remove this file
|
* something else. If you have configured your web server to remove this file
|
||||||
* from your site URIs, set this variable to an empty string.
|
* from your site URIs, set this variable to an empty string.
|
||||||
*/
|
*/
|
||||||
public string $indexPage = 'index.php';
|
public string $indexPage = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class Database extends Config
|
|||||||
'hostname' => 'localhost',
|
'hostname' => 'localhost',
|
||||||
'username' => 'root',
|
'username' => 'root',
|
||||||
'password' => '',
|
'password' => '',
|
||||||
'database' => 'doctor_appointment_system',
|
'database' => '',
|
||||||
'DBDriver' => 'MySQLi',
|
'DBDriver' => 'MySQLi',
|
||||||
'DBPrefix' => '',
|
'DBPrefix' => '',
|
||||||
'pConnect' => false,
|
'pConnect' => false,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ use CodeIgniter\Filters\InvalidChars;
|
|||||||
use CodeIgniter\Filters\PageCache;
|
use CodeIgniter\Filters\PageCache;
|
||||||
use CodeIgniter\Filters\PerformanceMetrics;
|
use CodeIgniter\Filters\PerformanceMetrics;
|
||||||
use CodeIgniter\Filters\SecureHeaders;
|
use CodeIgniter\Filters\SecureHeaders;
|
||||||
|
use App\Filters\AuthSession;
|
||||||
|
|
||||||
class Filters extends BaseFilters
|
class Filters extends BaseFilters
|
||||||
{
|
{
|
||||||
@ -34,6 +35,7 @@ class Filters extends BaseFilters
|
|||||||
'forcehttps' => ForceHTTPS::class,
|
'forcehttps' => ForceHTTPS::class,
|
||||||
'pagecache' => PageCache::class,
|
'pagecache' => PageCache::class,
|
||||||
'performance' => PerformanceMetrics::class,
|
'performance' => PerformanceMetrics::class,
|
||||||
|
'authSession' => AuthSession::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +74,7 @@ class Filters extends BaseFilters
|
|||||||
*/
|
*/
|
||||||
public array $globals = [
|
public array $globals = [
|
||||||
'before' => [
|
'before' => [
|
||||||
'csrf',
|
// 'csrf', // temporarily disabled for local testing
|
||||||
],
|
],
|
||||||
'after' => [
|
'after' => [
|
||||||
// 'honeypot',
|
// 'honeypot',
|
||||||
@ -104,5 +106,9 @@ class Filters extends BaseFilters
|
|||||||
*
|
*
|
||||||
* @var array<string, array<string, list<string>>>
|
* @var array<string, array<string, list<string>>>
|
||||||
*/
|
*/
|
||||||
public array $filters = [];
|
public array $filters = [
|
||||||
|
'authSession' => [
|
||||||
|
'before' => ['admin/*','doctor/*','patient/*','logout',],
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,33 @@ use CodeIgniter\Router\RouteCollection;
|
|||||||
*/
|
*/
|
||||||
// $routes->get('/', 'Home::index');
|
// $routes->get('/', 'Home::index');
|
||||||
$routes->get('/', 'Auth::login');
|
$routes->get('/', 'Auth::login');
|
||||||
$routes->post('/login', 'Auth::loginProcess');
|
$routes->post('/login', 'Auth::loginProcess', ['as' => 'login']);
|
||||||
$routes->get('/register', 'Auth::register');
|
$routes->get('/register', 'Auth::register');
|
||||||
$routes->post('/register', 'Auth::registerProcess');
|
$routes->post('/register', 'Auth::registerProcess');
|
||||||
|
|
||||||
$routes->get('/logout', 'Auth::logout');
|
$routes->get('/logout', 'Auth::logout');
|
||||||
$routes->get('/admin/dashboard', 'Admin::dashboard');
|
$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/patients', 'Admin::patients');
|
$routes->get('/admin/patients', 'Admin::patients');
|
||||||
$routes->get('/admin/deletePatient/(:num)', 'Admin::deletePatient/$1');
|
$routes->get('/admin/appointments', 'Admin::appointments');
|
||||||
$routes->get('/patient/dashboard', 'Patient::dashboard');
|
|
||||||
|
|
||||||
|
$routes->get('/admin/deleteDoctor/(:num)', 'Admin::deleteDoctor/$1');
|
||||||
|
$routes->get('/admin/deletePatient/(:num)', 'Admin::deletePatient/$1');
|
||||||
|
|
||||||
|
$routes->get('/patient/dashboard', 'Patient::dashboard');
|
||||||
|
$routes->post('/book-appointment', 'Patient::bookAppointment');
|
||||||
|
|
||||||
|
$routes->get('/doctor/dashboard', 'Doctor::dashboard');
|
||||||
|
$routes->get('/doctor/profile', 'Doctor::profile');
|
||||||
|
$routes->post('/doctor/profile', 'Doctor::profile');
|
||||||
|
$routes->post('/doctor/appointment/(:num)/accept', 'Doctor::accept/$1');
|
||||||
|
$routes->post('/doctor/appointment/(:num)/reject', 'Doctor::reject/$1');
|
||||||
|
|
||||||
|
$routes->get('/forgot-password', 'Auth::forgotPassword');
|
||||||
|
$routes->post('/forgot-password', 'Auth::processForgotPassword');
|
||||||
|
$routes->get('/reset-password/(:any)', 'Auth::resetPassword/$1');
|
||||||
|
$routes->post('/reset-password', 'Auth::processResetPassword');
|
||||||
|
|
||||||
|
$routes->get('/admin/dashboard', 'Admin::dashboard');
|
||||||
@ -3,7 +3,9 @@
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
|
use App\Models\DoctorModel;
|
||||||
use App\Models\PatientModel;
|
use App\Models\PatientModel;
|
||||||
|
use App\Models\AppointmentModel;
|
||||||
|
|
||||||
class Admin extends BaseController
|
class Admin extends BaseController
|
||||||
{
|
{
|
||||||
@ -12,10 +14,62 @@ class Admin extends BaseController
|
|||||||
if ($r = $this->requireRole('admin')) {
|
if ($r = $this->requireRole('admin')) {
|
||||||
return $r;
|
return $r;
|
||||||
}
|
}
|
||||||
$patientModel = new PatientModel();
|
|
||||||
|
$doctorModel = new DoctorModel();
|
||||||
|
$patientModel = new PatientModel();
|
||||||
|
$appointmentModel = new AppointmentModel();
|
||||||
|
|
||||||
|
$data['totalDoctors'] = $doctorModel->countAll();
|
||||||
$data['totalPatients'] = $patientModel->countAll();
|
$data['totalPatients'] = $patientModel->countAll();
|
||||||
|
$data['totalAppointments'] = $appointmentModel->countAll();
|
||||||
|
|
||||||
return view('admin/dashboard', $data);
|
return view('admin/dashboard', $data);
|
||||||
}
|
}
|
||||||
|
public function doctors()
|
||||||
|
{
|
||||||
|
if ($r = $this->requireRole('admin')) {
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$query = $db->query("
|
||||||
|
SELECT users.id, users.name, users.email, doctors.specialization
|
||||||
|
FROM users
|
||||||
|
JOIN doctors ON doctors.user_id = users.id
|
||||||
|
WHERE users.role = 'doctor'
|
||||||
|
");
|
||||||
|
|
||||||
|
$data['doctors'] = $query->getResult();
|
||||||
|
|
||||||
|
return view('admin/doctors', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteDoctor($id)
|
||||||
|
{
|
||||||
|
if ($r = $this->requireRole('admin')) {
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int) $id;
|
||||||
|
if ($id < 1) {
|
||||||
|
return redirect()->to(site_url('admin/doctors'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel = new UserModel();
|
||||||
|
$doctorModel = new DoctorModel();
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$doctor = $doctorModel->where('user_id', $id)->first();
|
||||||
|
if ($doctor) {
|
||||||
|
$db->table('appointments')->where('doctor_id', $doctor['id'])->delete();
|
||||||
|
$doctorModel->delete($doctor['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel->delete($id);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('admin/doctors'));
|
||||||
|
}
|
||||||
|
|
||||||
public function patients()
|
public function patients()
|
||||||
{
|
{
|
||||||
@ -36,7 +90,7 @@ class Admin extends BaseController
|
|||||||
|
|
||||||
return view('admin/patients', $data);
|
return view('admin/patients', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deletePatient($id)
|
public function deletePatient($id)
|
||||||
{
|
{
|
||||||
if ($r = $this->requireRole('admin')) {
|
if ($r = $this->requireRole('admin')) {
|
||||||
@ -63,4 +117,159 @@ class Admin extends BaseController
|
|||||||
return redirect()->to(site_url('admin/patients'));
|
return redirect()->to(site_url('admin/patients'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public function appointments()
|
||||||
|
{
|
||||||
|
if ($r = $this->requireRole('admin')) {
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$query = $db->query("
|
||||||
|
SELECT a.*, u1.name as patient_name, u2.name as doctor_name
|
||||||
|
FROM appointments a
|
||||||
|
JOIN patients p ON p.id = a.patient_id
|
||||||
|
JOIN users u1 ON u1.id = p.user_id
|
||||||
|
JOIN doctors d ON d.id = a.doctor_id
|
||||||
|
JOIN users u2 ON u2.id = d.user_id
|
||||||
|
");
|
||||||
|
|
||||||
|
$data['appointments'] = $query->getResult();
|
||||||
|
|
||||||
|
return view('admin/appointments', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDoctor()
|
||||||
|
{
|
||||||
|
if ($r = $this->requireRole('admin')) {
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin/add_doctor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeDoctor()
|
||||||
|
{
|
||||||
|
if ($r = $this->requireRole('admin')) {
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel = new UserModel();
|
||||||
|
$doctorModel = new DoctorModel();
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$validation = \Config\Services::validation();
|
||||||
|
|
||||||
|
$entries = $this->request->getPost('doctors');
|
||||||
|
if (! is_array($entries) || $entries === []) {
|
||||||
|
$entries = [[
|
||||||
|
'name' => $this->request->getPost('name'),
|
||||||
|
'email' => $this->request->getPost('email'),
|
||||||
|
'password' => $this->request->getPost('password'),
|
||||||
|
'specialization' => $this->request->getPost('specialization'),
|
||||||
|
'experience' => $this->request->getPost('experience'),
|
||||||
|
'fees' => $this->request->getPost('fees'),
|
||||||
|
'available_from' => $this->request->getPost('available_from'),
|
||||||
|
'available_to' => $this->request->getPost('available_to'),
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'name' => 'required|min_length[3]|max_length[100]',
|
||||||
|
'email' => 'required|valid_email',
|
||||||
|
'password' => 'required|min_length[8]',
|
||||||
|
'specialization' => 'required|min_length[2]|max_length[191]',
|
||||||
|
'experience' => 'permit_empty|max_length[100]',
|
||||||
|
'fees' => 'permit_empty|decimal',
|
||||||
|
'available_from' => 'permit_empty|valid_date[H:i]',
|
||||||
|
'available_to' => 'permit_empty|valid_date[H:i]',
|
||||||
|
];
|
||||||
|
|
||||||
|
$emailsSeen = [];
|
||||||
|
$cleanRows = [];
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
foreach ($entries as $i => $row) {
|
||||||
|
$row = [
|
||||||
|
'name' => trim((string) ($row['name'] ?? '')),
|
||||||
|
'email' => trim((string) ($row['email'] ?? '')),
|
||||||
|
'password' => (string) ($row['password'] ?? ''),
|
||||||
|
'specialization' => trim((string) ($row['specialization'] ?? '')),
|
||||||
|
'experience' => trim((string) ($row['experience'] ?? '')),
|
||||||
|
'fees' => trim((string) ($row['fees'] ?? '')),
|
||||||
|
'available_from' => trim((string) ($row['available_from'] ?? '')),
|
||||||
|
'available_to' => trim((string) ($row['available_to'] ?? '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
$rowNumber = $i + 1;
|
||||||
|
|
||||||
|
if (! $validation->setRules($rules)->run($row)) {
|
||||||
|
$rowErrors = $validation->getErrors();
|
||||||
|
$error = 'Row ' . $rowNumber . ': ' . implode(', ', array_values($rowErrors));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$emailKey = strtolower($row['email']);
|
||||||
|
if (isset($emailsSeen[$emailKey])) {
|
||||||
|
$error = 'Row ' . $rowNumber . ': Duplicate email in submitted rows.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$emailsSeen[$emailKey] = true;
|
||||||
|
|
||||||
|
if ($userModel->where('email', $row['email'])->first()) {
|
||||||
|
$error = 'Row ' . $rowNumber . ': Email already exists.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanRows[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error !== null || $cleanRows === []) {
|
||||||
|
return redirect()->back()->withInput()->with('error', $error ?? 'Please provide at least one doctor row.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->transStart();
|
||||||
|
|
||||||
|
foreach ($cleanRows as $row) {
|
||||||
|
$userData = [
|
||||||
|
'name' => $row['name'],
|
||||||
|
'email' => $row['email'],
|
||||||
|
'password' => password_hash($row['password'], PASSWORD_DEFAULT),
|
||||||
|
'role' => 'doctor',
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $userModel->skipValidation(true)->insert($userData)) {
|
||||||
|
$db->transRollback();
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->with('error', 'Could not create user for ' . $row['email'] . '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int) $userModel->getInsertID();
|
||||||
|
|
||||||
|
$doctorRow = [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'specialization' => $row['specialization'],
|
||||||
|
'experience' => $row['experience'] !== '' ? $row['experience'] : null,
|
||||||
|
'fees' => $row['fees'] !== '' ? $row['fees'] : null,
|
||||||
|
'available_from' => $row['available_from'] !== '' ? $row['available_from'] : null,
|
||||||
|
'available_to' => $row['available_to'] !== '' ? $row['available_to'] : null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $doctorModel->skipValidation(true)->insert($doctorRow)) {
|
||||||
|
$db->transRollback();
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->with('error', 'Could not create doctor profile for ' . $row['email'] . '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->transComplete();
|
||||||
|
|
||||||
|
if (! $db->transStatus()) {
|
||||||
|
return redirect()->back()->withInput()->with('error', 'Transaction failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = count($cleanRows);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('admin/doctors'))->with('success', $created . ' doctor account(s) created.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -20,8 +20,9 @@ class Auth extends BaseController
|
|||||||
public function registerProcess()
|
public function registerProcess()
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'name' => 'required|min_length[3]|max_length[100]',
|
'name' => 'required|min_length[3]|max_length[100]|alpha_numeric_punct',
|
||||||
'email' => 'required|valid_email|is_unique[users.email]',
|
'email' => 'required|valid_email|is_unique[users.email]',
|
||||||
|
'phone' => 'required|min_length[10]|max_length[10]',
|
||||||
'password' => 'required|min_length[8]',
|
'password' => 'required|min_length[8]',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ class Auth extends BaseController
|
|||||||
'email' => $this->request->getPost('email'),
|
'email' => $this->request->getPost('email'),
|
||||||
'password' => password_hash((string) $this->request->getPost('password'), PASSWORD_DEFAULT),
|
'password' => password_hash((string) $this->request->getPost('password'), PASSWORD_DEFAULT),
|
||||||
'role' => 'patient',
|
'role' => 'patient',
|
||||||
|
'status' => 'active',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! $userModel->skipValidation(true)->insert($data)) {
|
if (! $userModel->skipValidation(true)->insert($data)) {
|
||||||
@ -45,7 +47,10 @@ class Auth extends BaseController
|
|||||||
$user_id = $userModel->getInsertID();
|
$user_id = $userModel->getInsertID();
|
||||||
|
|
||||||
$patientModel = new PatientModel();
|
$patientModel = new PatientModel();
|
||||||
$patientModel->insert(['user_id' => $user_id]);
|
$patientModel->insert([
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'phone' => $this->request->getPost('phone'),
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->to(site_url('/'))->with('success', 'Account created. You can log in now.');
|
return redirect()->to(site_url('/'))->with('success', 'Account created. You can log in now.');
|
||||||
}
|
}
|
||||||
@ -69,9 +74,17 @@ class Auth extends BaseController
|
|||||||
$user = $userModel->where('email', $email)->first();
|
$user = $userModel->where('email', $email)->first();
|
||||||
|
|
||||||
if ($user && password_verify((string) $password, $user['password'])) {
|
if ($user && password_verify((string) $password, $user['password'])) {
|
||||||
|
$loginToken = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
if (! $userModel->update($user['id'], ['session_token' => $loginToken])) {
|
||||||
|
return redirect()->back()->withInput()->with('error', 'Could not start session. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->regenerate();
|
||||||
session()->set([
|
session()->set([
|
||||||
'id' => $user['id'],
|
'id' => $user['id'],
|
||||||
'role' => $user['role'],
|
'role' => $user['role'],
|
||||||
|
'login_token' => $loginToken,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($user['role'] === 'admin') {
|
if ($user['role'] === 'admin') {
|
||||||
@ -89,8 +102,93 @@ class Auth extends BaseController
|
|||||||
|
|
||||||
public function logout()
|
public function logout()
|
||||||
{
|
{
|
||||||
|
$userId = (int) session()->get('id');
|
||||||
|
$token = (string) session()->get('login_token');
|
||||||
|
|
||||||
|
if ($userId > 0 && $token !== '') {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->table('users')
|
||||||
|
->where('id', $userId)
|
||||||
|
->where('session_token', $token)
|
||||||
|
->update(['session_token' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
session()->destroy();
|
session()->destroy();
|
||||||
|
|
||||||
return redirect()->to(site_url('/'));
|
return redirect()->to(site_url('/'));
|
||||||
}
|
}
|
||||||
|
public function forgotPassword()
|
||||||
|
{
|
||||||
|
return view('auth/forgot_password');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processForgotPassword()
|
||||||
|
{
|
||||||
|
$rules = [
|
||||||
|
'email' => 'required|valid_email',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel = new UserModel();
|
||||||
|
$email = $this->request->getPost('email');
|
||||||
|
$user = $userModel->where('email', $email)->first();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return redirect()->back()->with('error', 'Email not found.');
|
||||||
|
}
|
||||||
|
$resetToken = bin2hex(random_bytes(32));
|
||||||
|
$tokenExpires = date('Y-m-d H:i:s', strtotime('+30 minutes'));
|
||||||
|
|
||||||
|
$userModel->update($user['id'], [
|
||||||
|
'reset_token' => $resetToken,
|
||||||
|
'reset_token_expires' => $tokenExpires,
|
||||||
|
]);
|
||||||
|
$resetLink = site_url("reset-password/$resetToken");
|
||||||
|
return redirect()->back()->with('success', "Reset link: <a href='$resetLink'>$resetLink</a>");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetPassword($token)
|
||||||
|
{
|
||||||
|
$userModel = new UserModel();
|
||||||
|
$user = $userModel->where('reset_token', $token)->first();
|
||||||
|
|
||||||
|
if (! $user || strtotime($user['reset_token_expires']) < time()) {
|
||||||
|
return redirect()->to(site_url('/'))->with('error', 'Invalid or expired reset link.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('auth/reset_password', ['token' => $token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processResetPassword()
|
||||||
|
{
|
||||||
|
$rules = [
|
||||||
|
'token' => 'required',
|
||||||
|
'password' => 'required|min_length[8]',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $this->validate($rules)) {
|
||||||
|
return redirect()->back()->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel = new UserModel();
|
||||||
|
$token = $this->request->getPost('token');
|
||||||
|
$newPassword = $this->request->getPost('password');
|
||||||
|
|
||||||
|
$user = $userModel->where('reset_token', $token)->first();
|
||||||
|
|
||||||
|
if (! $user || strtotime($user['reset_token_expires']) < time()) {
|
||||||
|
return redirect()->to(site_url('/'))->with('error', 'Invalid or expired reset link.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel->update($user['id'], [
|
||||||
|
'password' => password_hash($newPassword, PASSWORD_DEFAULT),
|
||||||
|
'reset_token' => null,
|
||||||
|
'reset_token_expires' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->to(site_url('/'))->with('success', 'Password reset successful. You can now login.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -132,7 +132,7 @@ class Doctor extends BaseController
|
|||||||
return redirect()->back()->with('error', 'Invalid appointment.');
|
return redirect()->back()->with('error', 'Invalid appointment.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = \App\Models\AppointmentModel::normalizeStatus($status);
|
$status = AppointmentModel::normalizeStatus($status);
|
||||||
$appointmentModel->update($appointmentId, ['status' => $status]);
|
$appointmentModel->update($appointmentId, ['status' => $status]);
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Appointment updated.');
|
return redirect()->back()->with('success', 'Appointment updated.');
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\AppointmentModel;
|
||||||
use App\Models\PatientModel;
|
use App\Models\PatientModel;
|
||||||
|
|
||||||
class Patient extends BaseController
|
class Patient extends BaseController
|
||||||
@ -43,7 +43,7 @@ class Patient extends BaseController
|
|||||||
JOIN doctors ON doctors.id = a.doctor_id
|
JOIN doctors ON doctors.id = a.doctor_id
|
||||||
JOIN users u ON u.id = doctors.user_id
|
JOIN users u ON u.id = doctors.user_id
|
||||||
WHERE a.patient_id = ?
|
WHERE a.patient_id = ?
|
||||||
ORDER BY a.appointment_date DESC, a.appointment_time DESC
|
ORDER BY a.appointment_date ASC, a.appointment_time ASC
|
||||||
', [$patient['id']])->getResult();
|
', [$patient['id']])->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ class Patient extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'doctor_id' => 'required|integer',
|
'doctor_id' => 'required|integer',
|
||||||
'date' => 'required|valid_date',
|
'date' => 'required|valid_date',
|
||||||
'time' => 'required',
|
'time' => 'required',
|
||||||
];
|
];
|
||||||
@ -66,7 +66,7 @@ class Patient extends BaseController
|
|||||||
return redirect()->back()->withInput();
|
return redirect()->back()->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$appointmentModel = new AppointmentModel();
|
||||||
$patientModel = new PatientModel();
|
$patientModel = new PatientModel();
|
||||||
|
|
||||||
$userId = (int) session()->get('id');
|
$userId = (int) session()->get('id');
|
||||||
@ -85,6 +85,21 @@ class Patient extends BaseController
|
|||||||
'appointment_time' => $appointmentTime,
|
'appointment_time' => $appointmentTime,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$taken = $appointmentModel
|
||||||
|
->where('doctor_id', $data['doctor_id'])
|
||||||
|
->where('appointment_date', $data['appointment_date'])
|
||||||
|
->where('appointment_time', $appointmentTime)
|
||||||
|
->whereIn('status', ['pending', 'approved'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($taken) {
|
||||||
|
return redirect()->back()->withInput()->with('error', 'That time slot is already booked for this doctor. Please choose another date or time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $appointmentModel->insert($data)) {
|
||||||
|
return redirect()->back()->withInput()->with('error', 'Could not book appointment.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to(site_url('patient/dashboard'))->with('success', 'Appointment requested.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class InitAppointmentSchema extends Migration
|
|||||||
'doctor_id' => ['type' => 'INT', 'unsigned' => true],
|
'doctor_id' => ['type' => 'INT', 'unsigned' => true],
|
||||||
'appointment_date' => ['type' => 'DATE'],
|
'appointment_date' => ['type' => 'DATE'],
|
||||||
'appointment_time' => ['type' => 'TIME'],
|
'appointment_time' => ['type' => 'TIME'],
|
||||||
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'pending'],
|
'status' => ['type' => 'ENUM', 'constraint' => ['pending', 'approved', 'rejected'], 'default' => 'pending'],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addKey('id', true);
|
||||||
$this->forge->addKey(['doctor_id', 'appointment_date', 'appointment_time']);
|
$this->forge->addKey(['doctor_id', 'appointment_date', 'appointment_time']);
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddPasswordResetColumns extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->forge->addColumn('users', [
|
||||||
|
'reset_token' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||||
|
'reset_token_expires' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->forge->dropColumn('users', ['reset_token', 'reset_token_expires']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class UpdateAppointmentStatusEnum extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Ensure enum values match the application status values.
|
||||||
|
$this->forge->modifyColumn('appointments', [
|
||||||
|
'status' => [
|
||||||
|
'name' => 'status',
|
||||||
|
'type' => 'ENUM',
|
||||||
|
'constraint' => ['pending', 'approved', 'rejected'],
|
||||||
|
'default' => 'pending',
|
||||||
|
'null' => false,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Convert legacy status values
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->query("UPDATE `appointments` SET `status`='pending' WHERE `status` IS NULL OR `status`='' OR `status` NOT IN ('pending', 'approved', 'rejected')");
|
||||||
|
$db->query("UPDATE `appointments` SET `status`='approved' WHERE `status`='confirmed'");
|
||||||
|
$db->query("UPDATE `appointments` SET `status`='rejected' WHERE `status`='cancelled'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->forge->modifyColumn('appointments', [
|
||||||
|
'status' => [
|
||||||
|
'name' => 'status',
|
||||||
|
'type' => 'ENUM',
|
||||||
|
'constraint' => ['pending', 'confirmed', 'cancelled'],
|
||||||
|
'default' => 'pending',
|
||||||
|
'null' => false,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddSessionTokenToUsers extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$fields = [
|
||||||
|
'session_token' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 128,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'status',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->forge->addColumn('users', $fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->forge->dropColumn('users', 'session_token');
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Filters/AuthSession.php
Normal file
41
app/Filters/AuthSession.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filters;
|
||||||
|
|
||||||
|
use App\Models\UserModel;
|
||||||
|
use CodeIgniter\Filters\FilterInterface;
|
||||||
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
|
||||||
|
class AuthSession implements FilterInterface
|
||||||
|
{
|
||||||
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
|
{
|
||||||
|
$session = session();
|
||||||
|
$userId = (int) $session->get('id');
|
||||||
|
$role = (string) $session->get('role');
|
||||||
|
$token = (string) $session->get('login_token');
|
||||||
|
|
||||||
|
if ($userId < 1 || $role === '' || $token === '') {
|
||||||
|
$session->destroy();
|
||||||
|
|
||||||
|
return redirect()->to(site_url('/'))->with('error', 'Please login first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userModel = new UserModel();
|
||||||
|
$user = $userModel->where('id', $userId)->where('role', $role)->first();
|
||||||
|
|
||||||
|
if (! $user || empty($user['session_token']) || ! hash_equals((string) $user['session_token'], $token)) {
|
||||||
|
$session->destroy();
|
||||||
|
|
||||||
|
return redirect()->to(site_url('/'))->with('error', 'Your session ended because your account logged in from another device/browser.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
|
{
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/Models/AppointmentModel.php
Normal file
76
app/Models/AppointmentModel.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class AppointmentModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'appointments';
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
protected $useAutoIncrement = true;
|
||||||
|
protected $returnType = 'array';
|
||||||
|
protected $useSoftDeletes = false;
|
||||||
|
protected $protectFields = true;
|
||||||
|
protected $allowedFields = [
|
||||||
|
'patient_id',
|
||||||
|
'doctor_id',
|
||||||
|
'appointment_date',
|
||||||
|
'appointment_time',
|
||||||
|
'status'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected bool $allowEmptyInserts = false;
|
||||||
|
protected bool $updateOnlyChanged = true;
|
||||||
|
|
||||||
|
protected array $casts = [];
|
||||||
|
protected array $castHandlers = [];
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
protected $dateFormat = 'datetime';
|
||||||
|
protected $createdField = 'created_at';
|
||||||
|
protected $updatedField = 'updated_at';
|
||||||
|
protected $deletedField = 'deleted_at';
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
protected $validationRules = [];
|
||||||
|
protected $validationMessages = [];
|
||||||
|
protected $skipValidation = false;
|
||||||
|
protected $cleanValidationRules = true;
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
protected $allowCallbacks = true;
|
||||||
|
protected $beforeInsert = ['setDefaultStatus'];
|
||||||
|
protected $afterInsert = [];
|
||||||
|
|
||||||
|
protected static array $validStatuses = ['pending', 'approved', 'rejected'];
|
||||||
|
|
||||||
|
public static function normalizeStatus(?string $status): string
|
||||||
|
{
|
||||||
|
$status = trim((string) $status);
|
||||||
|
|
||||||
|
if ($status === '' || ! in_array($status, self::$validStatuses, true)) {
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setDefaultStatus(array $eventData): array
|
||||||
|
{
|
||||||
|
if (! isset($eventData['data']['status']) || trim((string) $eventData['data']['status']) === '') {
|
||||||
|
$eventData['data']['status'] = 'pending';
|
||||||
|
} else {
|
||||||
|
$eventData['data']['status'] = self::normalizeStatus($eventData['data']['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $eventData;
|
||||||
|
}
|
||||||
|
protected $beforeUpdate = [];
|
||||||
|
protected $afterUpdate = [];
|
||||||
|
protected $beforeFind = [];
|
||||||
|
protected $afterFind = [];
|
||||||
|
protected $beforeDelete = [];
|
||||||
|
protected $afterDelete = [];
|
||||||
|
}
|
||||||
@ -13,7 +13,14 @@ class UserModel extends Model
|
|||||||
protected $useSoftDeletes = false;
|
protected $useSoftDeletes = false;
|
||||||
protected $protectFields = true;
|
protected $protectFields = true;
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'name','email','password','role','status'
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'role',
|
||||||
|
'status',
|
||||||
|
'session_token',
|
||||||
|
'reset_token',
|
||||||
|
'reset_token_expires',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected bool $allowEmptyInserts = false;
|
protected bool $allowEmptyInserts = false;
|
||||||
|
|||||||
@ -7,88 +7,143 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
|
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
|
||||||
</head>
|
</head>
|
||||||
<body class="app-body app-page--admin">
|
<body class="app-body app-page--admin"5>
|
||||||
|
|
||||||
<?php $validationErrors = validation_errors(); ?>
|
<?php
|
||||||
|
$oldDoctors = old('doctors');
|
||||||
|
if (! is_array($oldDoctors) || $oldDoctors === []) {
|
||||||
|
$oldDoctors = [[
|
||||||
|
'name' => '',
|
||||||
|
'email' => '',
|
||||||
|
'password' => '',
|
||||||
|
'specialization' => '',
|
||||||
|
'experience' => '',
|
||||||
|
'fees' => '',
|
||||||
|
'available_from' => '',
|
||||||
|
'available_to' => '',
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<div class="container py-5" style="max-width: 560px;">
|
<div class="container py-5" style="max-width: 980px;">
|
||||||
|
|
||||||
<h2 class="text-center mb-4 app-heading">Add doctor</h2>
|
<h2 class="text-center mb-4 app-heading">Add Doctors</h2>
|
||||||
|
|
||||||
<?php if (session()->getFlashdata('error')): ?>
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
|
<div class="alert alert-danger app-alert text-center"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="post" action="<?= base_url('admin/doctors/add') ?>" class="app-form card app-card-dashboard p-4" 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() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<label class="form-label" for="name">Full name</label>
|
<p class="mb-0 text-muted">Add one or many doctors, then submit once.</p>
|
||||||
<input type="text" name="name" id="name" value="<?= esc(old('name')) ?>"
|
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3" id="add-doctor-row">+ Add another doctor</button>
|
||||||
class="form-control <?= isset($validationErrors['name']) ? 'is-invalid' : '' ?>">
|
|
||||||
<?= validation_show_error('name') ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div id="doctor-rows">
|
||||||
<label class="form-label" for="email">Email (login)</label>
|
<?php foreach ($oldDoctors as $index => $doctor): ?>
|
||||||
<input type="text" name="email" id="email" value="<?= esc(old('email')) ?>"
|
<div class="border rounded-4 p-3 mb-3 doctor-row" data-row>
|
||||||
class="form-control <?= isset($validationErrors['email']) ? 'is-invalid' : '' ?>" autocomplete="off">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<?= validation_show_error('email') ?>
|
<h6 class="mb-0">Doctor No: <span data-row-number><?= $index + 1 ?></span></h6>
|
||||||
</div>
|
<button type="button" class="btn btn-sm btn-outline-danger remove-row" <?= $index === 0 ? 'style="display:none"' : '' ?>>Remove</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row g-3">
|
||||||
<label class="form-label" for="password"> Password</label>
|
<div class="col-md-6">
|
||||||
<input type="password" name="password" id="password"
|
<label class="form-label">Full name</label>
|
||||||
class="form-control <?= isset($validationErrors['password']) ? 'is-invalid' : '' ?>" autocomplete="new-password">
|
<input type="text" name="doctors[<?= $index ?>][name]" value="<?= esc($doctor['name'] ?? '') ?>" class="form-control">
|
||||||
<?= validation_show_error('password') ?>
|
</div>
|
||||||
</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="mb-3">
|
<div class="col-md-6">
|
||||||
<label class="form-label" for="specialization">Specialization</label>
|
<label class="form-label">Password</label>
|
||||||
<input type="text" name="specialization" id="specialization" value="<?= esc(old('specialization')) ?>"
|
<input type="password" name="doctors[<?= $index ?>][password]" value="<?= esc($doctor['password'] ?? '') ?>" class="form-control" autocomplete="new-password">
|
||||||
class="form-control <?= isset($validationErrors['specialization']) ? 'is-invalid' : '' ?>"
|
</div>
|
||||||
placeholder="e.g. Cardiology">
|
<div class="col-md-6">
|
||||||
<?= validation_show_error('specialization') ?>
|
<label class="form-label">Specialization</label>
|
||||||
</div>
|
<input type="text" name="doctors[<?= $index ?>][specialization]" value="<?= esc($doctor['specialization'] ?? '') ?>" class="form-control" placeholder="e.g. Cardiology">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="col-md-4">
|
||||||
<label class="form-label" for="experience">Experience (optional)</label>
|
<label class="form-label">Experience (optional)</label>
|
||||||
<input type="text" name="experience" id="experience" value="<?= esc(old('experience')) ?>"
|
<input type="text" name="doctors[<?= $index ?>][experience]" value="<?= esc($doctor['experience'] ?? '') ?>" class="form-control" placeholder="e.g. 10 years">
|
||||||
class="form-control"
|
</div>
|
||||||
placeholder="e.g. 10 years">
|
<div class="col-md-4">
|
||||||
<?= validation_show_error('experience') ?>
|
<label class="form-label">Consultation fee (optional)</label>
|
||||||
</div>
|
<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="mb-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label" for="fees">Consultation fee (optional)</label>
|
<label class="form-label">Available from</label>
|
||||||
<input type="number" name="fees" id="fees" value="<?= esc(old('fees')) ?>"
|
<input type="time" name="doctors[<?= $index ?>][available_from]" value="<?= esc($doctor['available_from'] ?? '') ?>" class="form-control">
|
||||||
class="form-control"
|
</div>
|
||||||
placeholder="e.g. 500.00" step="0.01" min="0">
|
<div class="col-md-2">
|
||||||
<?= validation_show_error('fees') ?>
|
<label class="form-label">Available to</label>
|
||||||
</div>
|
<input type="time" name="doctors[<?= $index ?>][available_to]" value="<?= esc($doctor['available_to'] ?? '') ?>" class="form-control">
|
||||||
|
</div>
|
||||||
<div class="row">
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
</div>
|
||||||
<label class="form-label" for="available_from">Available from</label>
|
<?php endforeach; ?>
|
||||||
<input type="time" name="available_from" id="available_from" value="<?= esc(old('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')) ?>"
|
|
||||||
class="form-control <?= isset($validationErrors['available_to']) ? 'is-invalid' : '' ?>">
|
|
||||||
<?= validation_show_error('available_to') ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between mt-4">
|
<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>
|
<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 doctor</button>
|
<button type="submit" class="btn btn-app-primary px-4">Create doctors</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const rowsContainer = document.getElementById('doctor-rows');
|
||||||
|
const addBtn = document.getElementById('add-doctor-row');
|
||||||
|
|
||||||
|
const updateRowNumbers = () => {
|
||||||
|
const rows = rowsContainer.querySelectorAll('[data-row]');
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
const numberEl = row.querySelector('[data-row-number]');
|
||||||
|
if (numberEl) numberEl.textContent = String(index + 1);
|
||||||
|
row.querySelectorAll('input').forEach((input) => {
|
||||||
|
const name = input.getAttribute('name');
|
||||||
|
if (!name) return;
|
||||||
|
input.setAttribute('name', name.replace(/doctors\[\d+]/, `doctors[${index}]`));
|
||||||
|
});
|
||||||
|
const removeBtn = row.querySelector('.remove-row');
|
||||||
|
if (removeBtn) {
|
||||||
|
removeBtn.style.display = rows.length === 1 ? 'none' : 'inline-block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addBtn.addEventListener('click', () => {
|
||||||
|
const firstRow = rowsContainer.querySelector('[data-row]');
|
||||||
|
if (!firstRow) return;
|
||||||
|
const clone = firstRow.cloneNode(true);
|
||||||
|
clone.querySelectorAll('input').forEach((input) => {
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
rowsContainer.appendChild(clone);
|
||||||
|
updateRowNumbers();
|
||||||
|
});
|
||||||
|
|
||||||
|
rowsContainer.addEventListener('click', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLElement) || !target.classList.contains('remove-row')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = target.closest('[data-row]');
|
||||||
|
if (!row) return;
|
||||||
|
row.remove();
|
||||||
|
updateRowNumbers();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRowNumbers();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
54
app/Views/admin/appointments.php
Normal file
54
app/Views/admin/appointments.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<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="<?= base_url('css/app.css') ?>">
|
||||||
|
</head>
|
||||||
|
<body class="app-body app-page--admin">
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
|
||||||
|
<h2 class="text-center mb-4 app-heading">Appointments</h2>
|
||||||
|
|
||||||
|
<div class="app-table-wrap">
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover text-center align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sl No</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Doctor</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<?php $i = 1; ?>
|
||||||
|
<?php foreach ($appointments as $a): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $i++ ?></td>
|
||||||
|
<td><?= esc($a->patient_name) ?></td>
|
||||||
|
<td><?= esc($a->doctor_name) ?></td>
|
||||||
|
<td><?= esc($a->appointment_date) ?></td>
|
||||||
|
<td><?= esc($a->appointment_time) ?></td>
|
||||||
|
<?php $status = trim((string) ($a->status ?? '')); ?>
|
||||||
|
<td><?= esc(ucfirst($status === '' ? 'pending' : $status)) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<a href="<?= base_url('admin/dashboard') ?>" class="btn btn-app-outline px-4">Back to dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<table class="table table-bordered table-hover text-center align-middle">
|
<table class="table table-bordered table-hover text-center align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>Sl No</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Phone</th>
|
<th>Phone</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
|
|||||||
50
app/Views/auth/forgot_password.php
Normal file
50
app/Views/auth/forgot_password.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Forgot Password</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') ?>">
|
||||||
|
</head>
|
||||||
|
<body class="app-body app-page--auth">
|
||||||
|
|
||||||
|
<div class="card auth-card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-center mb-4 auth-title">Forgot Password</h2>
|
||||||
|
|
||||||
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
|
<div class="alert alert-success app-alert">
|
||||||
|
<?= 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; ?>
|
||||||
|
|
||||||
|
<form method="post" action="<?= base_url('forgot-password') ?>" class="app-form" novalidate>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email Address</label>
|
||||||
|
<input type="email" name="email" id="email" class="form-control"
|
||||||
|
value="<?= esc(old('email')) ?>" required>
|
||||||
|
<?php if (session()->has('errors.email')): ?>
|
||||||
|
<div class="text-danger small mt-1"><?= session('errors.email') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Send Reset Link</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p class="text-muted">
|
||||||
|
Remember your password? <a href="<?= base_url('/') ?>">Login here</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -21,8 +21,8 @@
|
|||||||
<?php if (session()->getFlashdata('error')): ?>
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
<div class="alert alert-danger app-alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="post" action="<?= base_url('login') ?>" class="app-form" novalidate>
|
<form method="post" action="<?= route_to('login') ?>" class="app-form" novalidate>
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -44,6 +44,10 @@
|
|||||||
<button type="submit" class="btn btn-app-primary w-100">Login</button>
|
<button type="submit" class="btn btn-app-primary w-100">Login</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="<?= base_url('forgot-password') ?>" class="btn btn-link">Forgot Password?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<p class="auth-divider-text mb-2">Don't have an account?</p>
|
<p class="auth-divider-text mb-2">Don't have an account?</p>
|
||||||
<a href="<?= base_url('register') ?>" class="btn-app-secondary">Create account</a>
|
<a href="<?= base_url('register') ?>" class="btn-app-secondary">Create account</a>
|
||||||
|
|||||||
@ -38,6 +38,14 @@
|
|||||||
<?= validation_show_error('email') ?>
|
<?= validation_show_error('email') ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="phone" class="form-label">Phone</label>
|
||||||
|
<input type="phone" name="phone" id="phone" value="<?= esc(old('phone')) ?>"
|
||||||
|
class="form-control <?= isset($validationErrors['phone']) ? 'is-invalid' : '' ?>"
|
||||||
|
placeholder="Phone number" autocomplete="tel" required>
|
||||||
|
<?= validation_show_error('phone') ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Password</label>
|
<label for="password" class="form-label">Password</label>
|
||||||
<input type="password" name="password" id="password"
|
<input type="password" name="password" id="password"
|
||||||
|
|||||||
49
app/Views/auth/reset_password.php
Normal file
49
app/Views/auth/reset_password.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Reset Password</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') ?>">
|
||||||
|
</head>
|
||||||
|
<body class="app-body app-page--auth">
|
||||||
|
|
||||||
|
<div class="card auth-card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-center mb-4 auth-title">Reset Password</h2>
|
||||||
|
|
||||||
|
<?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; ?>
|
||||||
|
|
||||||
|
<form method="post" action="<?= base_url('reset-password') ?>" class="app-form" novalidate>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<input type="hidden" name="token" value="<?= esc($token) ?>">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">New Password</label>
|
||||||
|
<input type="password" name="password" id="password" class="form-control"
|
||||||
|
placeholder="Enter new password (min 8 characters)" required>
|
||||||
|
<?php if (session()->has('errors.password')): ?>
|
||||||
|
<div class="text-danger small mt-1"><?= session('errors.password') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Reset Password</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p class="text-muted">
|
||||||
|
<a href="<?= base_url('/') ?>">Back to Login</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user