2026-05-08 12:22:08 +05:30

1726 lines
63 KiB
PHP

<?php
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\DoctorBookingModel;
// ...existing code...
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 normalizeAppointmentTime(string $time): ?string
{
$time = trim($time);
if (preg_match('/^\d{2}:\d{2}$/', $time)) {
return $time . ':00';
}
return preg_match('/^\d{2}:\d{2}:\d{2}$/', $time) ? $time : null;
}
private function isPastAppointmentDateTime(string $date, string $time): bool
{
$timezone = new \DateTimeZone(config('App')->appTimezone);
$appointmentDateTime = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date . ' ' . $time, $timezone);
if (! $appointmentDateTime || $appointmentDateTime->format('Y-m-d H:i:s') !== $date . ' ' . $time) {
return true;
}
return $appointmentDateTime < new \DateTimeImmutable('now', $timezone);
}
private function doctorHasAvailabilityAt(int $doctorId, string $date, string $time): bool
{
try {
$dayNumber = (int) (new \DateTimeImmutable($date))->format('N');
} catch (\Exception $e) {
return false;
}
return \Config\Database::connect()
->table('doctor_availability')
->where('doctor_id', $doctorId)
->where('day_number', $dayNumber)
->where('status', 1)
->where('start_time <=', $time)
->where('end_time >', $time)
->countAllResults() > 0;
}
private function normalizeServiceIds($services): array
{
if (is_string($services)) {
$decoded = json_decode($services, true);
$services = is_array($decoded) ? $decoded : [$services];
}
if (! is_array($services)) {
return [
'ids' => [],
'has_duplicates' => false,
];
}
$ids = array_values(array_filter(array_map('intval', $services), static fn ($id) => $id > 0));
return [
'ids' => array_values(array_unique($ids)),
'has_duplicates' => count($ids) !== count(array_unique($ids)),
];
}
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')) {
return $r;
}
$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');
}
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();
$appointmentModel = new AppointmentModel();
$doctorSpecializationModel = new DoctorSpecializationModel();
$doctor = $doctorModel->findByUserId($id);
if ($doctor) {
$appointmentModel->deleteByDoctorId((int) $doctor['id']);
$doctorSpecializationModel->deleteByDoctorId((int) $doctor['id']);
$doctorModel->delete($doctor['id']);
}
$userModel->delete($id);
return redirect()->to(site_url('admin/doctors'));
}
public function patients()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$specializationModel = new SpecializationModel();
$specializations = $specializationModel->getOptionNames();
return view('admin/patients', [
'specializations' => $specializations,
]);
}
public function deletePatient($id)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = (int) $id;
if ($id < 1) {
return redirect()->to(site_url('admin/patients'));
}
$userModel = new UserModel();
$patientModel = new PatientModel();
$appointmentModel = new AppointmentModel();
$patient = $patientModel->findByUserId($id);
if ($patient) {
$appointmentModel->deleteByPatientId((int) $patient['id']);
$patientModel->delete($patient['id']);
}
$userModel->delete($id);
return redirect()->to(site_url('admin/patients'));
}
public function appointments()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$appointmentModel = new AppointmentModel();
$data['appointments'] = $appointmentModel->getAdminAppointments();
return view('admin/appointments', $data);
}
public function appointmentPreview()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$patientId = (int) $this->request->getGet('patient_id');
$rawServices = $this->request->getGet('services');
$preferredDoctorId = (int) ($this->request->getGet('doctor_id') ?? 0);
$preferredDoctorName = trim((string) ($this->request->getGet('doctor_name') ?? ''));
$requestedDate = (string) ($this->request->getGet('date') ?? '');
$requestedTime = (string) ($this->request->getGet('time') ?? '');
$serviceData = $this->normalizeServiceIds($rawServices);
$serviceIds = $serviceData['ids'];
$patient = null;
if ($patientId > 0) {
$patient = \Config\Database::connect()
->table('patients p')
->select("
p.id as patient_id,
p.user_id,
p.phone,
p.dob,
p.gender as patient_gender,
u.first_name,
u.last_name,
u.email,
u.gender as user_gender,
u.status,
CASE
WHEN u.formatted_user_id IS NULL
OR u.formatted_user_id = ''
OR u.formatted_user_id LIKE 'PHY%'
THEN CONCAT('PT', LPAD(u.id, 7, '0'))
ELSE u.formatted_user_id
END as formatted_user_id
")
->join('users u', 'u.id = p.user_id', 'left')
->where('p.id', $patientId)
->get()
->getRowArray();
}
$serviceMap = [];
$specializationModel = new SpecializationModel();
foreach ($specializationModel->findAll() as $specialization) {
$id = (int) ($specialization['id'] ?? 0);
$name = trim((string) ($specialization['name'] ?? ''));
if ($id > 0 && $name !== '') {
$serviceMap[$id] = $name;
}
}
$serviceNames = [];
foreach ($serviceIds as $serviceId) {
if (isset($serviceMap[$serviceId])) {
$serviceNames[] = $serviceMap[$serviceId];
}
}
if ($preferredDoctorName === '' && $preferredDoctorId > 0) {
$preferredDoctorName = \Config\Database::connect()
->table('doctors d')
->select("TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS doctor_name")
->join('users u', 'u.id = d.user_id', 'left')
->where('d.id', $preferredDoctorId)
->get()
->getRowArray()['doctor_name'] ?? '';
$preferredDoctorName = trim((string) $preferredDoctorName);
}
return view('admin/appointment_preview', [
'patient' => $patient,
'patient_id' => $patientId,
'services' => $serviceIds,
'service_names' => array_values(array_unique($serviceNames)),
'date' => $requestedDate,
'time' => $requestedTime,
'doctor' => $preferredDoctorName,
'preview_id' => 0,
]);
}
public function deleteAppointment($id)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = (int) $id;
if ($id < 1) {
return redirect()->to(site_url('admin/appointments'))->with('error', 'Invalid appointment.');
}
$appointmentModel = new AppointmentModel();
$bookingModel = new DoctorBookingModel();
$appointment = $appointmentModel->find($id);
if (! $appointment) {
return redirect()->to(site_url('admin/appointments'))->with('error', 'Appointment not found.');
}
// Update booking status if exists
$bookingModel->where('appointment_id', $id)->set(['status' => 'cancelled'])->update();
// ...existing code...
$previewModel = new \App\Models\AppointmentPreviewModel();
$previewModel
->where('patient_id', (int) ($appointment['patient_id'] ?? 0))
->where('assigned_doctor_id', (int) ($appointment['doctor_id'] ?? 0))
->where('requested_date', $appointment['appointment_date'] ?? null)
->where('requested_time', $appointment['appointment_time'] ?? null)
->set([
'status' => 'cancelled',
])
->update();
$appointmentModel->delete($id);
$logModel = new ActivityLogModel();
$logModel->log('delete_appointment', 'Admin deleted appointment ID ' . $id, 'appointment', $id);
return redirect()->to(site_url('admin/appointments'))->with('success', 'Appointment cleared successfully.');
}
public function updateAppointmentStatus()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = (int) $this->request->getPost('appointment_id');
$status = $this->request->getPost('status');
if ($id < 1 || empty($status)) {
return $this->response->setJSON(['success' => false, 'message' => 'Invalid data']);
}
$appointmentModel = new AppointmentModel();
$bookingModel = new DoctorBookingModel();
$appointment = $appointmentModel->find($id);
if (!$appointment) {
return $this->response->setJSON(['success' => false, 'message' => 'Appointment not found']);
}
$status = AppointmentModel::normalizeStatus($status);
$db = \Config\Database::connect();
$db->transStart();
$appointmentModel->update($id, ['status' => $status]);
// Sync with doctor_bookings table
$bookingModel->where('appointment_id', $id)->set(['status' => $status])->update();
$db->transComplete();
if ($db->transStatus() === false) {
return $this->response->setJSON(['success' => false, 'message' => 'Failed to update status']);
}
$logModel = new ActivityLogModel();
$logModel->log('update_appointment_status', "Admin updated appointment ID {$id} status to {$status}", 'appointment', $id);
return $this->response->setJSON(['success' => true, 'message' => 'Status updated successfully']);
}
public function createAppointment()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
// Check if this is a JSON request or form request
$isJson = $this->request->isAJAX() && stripos($this->request->getHeaderLine('Content-Type'), 'json') !== false;
if ($isJson) {
return $this->createAppointmentFromJson();
}
// Get the appointment_datetime and split it
$appointmentDateTime = $this->request->getPost('appointment_datetime');
if(!$appointmentDateTime){
return redirect()->back()
->withInput()
->with('error','Appointment date and time are required');
}
// Split the datetime-local format (YYYY-MM-DDTHH:mm)
$parts = explode('T', $appointmentDateTime);
$appointmentDate = $parts[0] ?? null;
$appointmentTime = isset($parts[1]) ? $this->normalizeAppointmentTime($parts[1]) : null;
if(!$appointmentDate || !$appointmentTime){
return redirect()->back()
->withInput()
->with('error','Invalid appointment date and time format');
}
if ($this->isPastAppointmentDateTime($appointmentDate, $appointmentTime)) {
return redirect()->back()
->withInput()
->with('error', 'Past date or time booking is not allowed.');
}
$appointmentModel = new AppointmentModel();
$serviceInput = $this->request->getPost('services');
$serviceData = $this->normalizeServiceIds($serviceInput);
if($serviceData['ids'] === []){
return redirect()->back()
->withInput()
->with('error','Please select at least one service');
}
if ($serviceData['has_duplicates']) {
return redirect()->back()
->withInput()
->with('error', 'Duplicate services are not allowed. Please select each service only once.');
}
$patientId = (int)$this->request->getPost('patient_id');
$doctorId = $this->request->getPost('doctor_id') ? (int)$this->request->getPost('doctor_id') : null;
$note = $this->request->getPost('note');
$data = [
'patient_id' => $patientId,
'doctor_id' => $doctorId,
'services' => json_encode($serviceData['ids']),
'created_by' => session()->get('id'),
'appointment_date' => $appointmentDate,
'appointment_time' => $appointmentTime,
'note' => !empty($note) ? trim($note) : null
];
// Only check for slot availability if doctor is selected
if($doctorId){
if (! $this->doctorHasAvailabilityAt($doctorId, $appointmentDate, $appointmentTime)) {
return redirect()->back()
->withInput()
->with('error', 'Doctor is not available at the selected time.');
}
$taken = $appointmentModel
->where('doctor_id',$doctorId)
->where('appointment_date',$appointmentDate)
->where('appointment_time',$appointmentTime)
->whereIn('status',['pending','approved'])
->first();
if($taken){
return redirect()->back()
->withInput()
->with('error','Slot already booked');
}
}
if($appointmentModel->insert($data)){
// Log activity
$activityLogModel = new ActivityLogModel();
$activityLogModel->log(
'appointment_created',
'Appointment created for patient ID ' . $data['patient_id'] . ' with services ' . $data['services'],
null,
null,
session()->get('id')
);
return redirect()
->to(site_url('admin/appointments'))
->with('success','Appointment created successfully');
} else {
return redirect()->back()
->withInput()
->with('error','Failed to create appointment');
}
}
private function createAppointmentFromJson()
{
$json = $this->request->getJSON();
$patientId = (int) ($json->patient_id ?? 0);
$doctorId = (int) ($json->doctor_id ?? 0);
$appointmentDate = (string) ($json->appointment_date ?? '');
$appointmentTime = $this->normalizeAppointmentTime((string) ($json->appointment_time ?? ''));
$note = (string) ($json->note ?? '');
$serviceData = $this->normalizeServiceIds($json->services ?? []);
if (!$patientId || !$doctorId || !$appointmentDate || !$appointmentTime) {
return $this->response->setJSON([
'success' => false,
'message' => 'Invalid appointment data'
]);
}
if ($this->isPastAppointmentDateTime($appointmentDate, $appointmentTime)) {
return $this->response->setJSON([
'success' => false,
'message' => 'Past date or time booking is not allowed'
]);
}
if ($serviceData['has_duplicates']) {
return $this->response->setJSON([
'success' => false,
'message' => 'Duplicate services are not allowed'
]);
}
$appointmentModel = new AppointmentModel();
$bookingModel = new DoctorBookingModel();
$patientModel = new PatientModel();
$doctorModel = new DoctorModel();
// Verify patient exists
$patient = $patientModel->find($patientId);
if (!$patient) {
return $this->response->setJSON([
'success' => false,
'message' => 'Patient not found'
]);
}
// Verify doctor exists
$doctor = $doctorModel->find($doctorId);
if (!$doctor) {
return $this->response->setJSON([
'success' => false,
'message' => 'Doctor not found'
]);
}
if (! $this->doctorHasAvailabilityAt($doctorId, $appointmentDate, $appointmentTime)) {
return $this->response->setJSON([
'success' => false,
'message' => 'Doctor is not available at the selected time'
]);
}
// Check if doctor is already booked using the source of truth (doctor_bookings table)
if ($bookingModel->isDoctorBooked($doctorId, $appointmentDate, $appointmentTime)) {
return $this->response->setJSON([
'success' => false,
'message' => 'Doctor already booked for this time'
]);
}
$data = [
'patient_id' => $patientId,
'doctor_id' => $doctorId,
'appointment_date' => $appointmentDate,
'appointment_time' => $appointmentTime,
'status' => 'assigned',
'note' => !empty($note) ? trim($note) : null
];
if ($serviceData['ids'] !== []) {
$data['services'] = json_encode($serviceData['ids']);
}
if ($appointmentModel->insert($data)) {
$appointmentId = (int) $appointmentModel->getInsertID();
// Sync with doctor_bookings table
$bookingModel->syncBooking($appointmentId, $doctorId, $appointmentDate, $appointmentTime, 'assigned', (int) session()->get('id'));
$previewId = (int) ($json->preview_id ?? 0);
if ($previewId > 0) {
$doctorUser = $doctor['user_id'] ? (new UserModel())->find((int) $doctor['user_id']) : null;
$assignedDoctorFormattedId = $doctorUser && ! empty($doctorUser['formatted_user_id'])
? $doctorUser['formatted_user_id']
: 'PHY' . str_pad((string) ($doctor['user_id'] ?: $doctorId), 7, '0', STR_PAD_LEFT);
// ...existing code...
$previewModel->update($previewId, [
'assigned_doctor_id' => $doctorId,
'assigned_doctor_formatted_id' => $assignedDoctorFormattedId,
'status' => 'booked',
]);
}
// Log activity
$activityLogModel = new ActivityLogModel();
$activityLogModel->log(
'appointment_created',
'Appointment created for patient ID ' . $patientId . ' with doctor ID ' . $doctorId,
'appointment',
$appointmentId,
session()->get('id')
);
return $this->response->setJSON([
'success' => true,
'message' => 'Appointment created successfully'
]);
} else {
return $this->response->setJSON([
'success' => false,
'message' => 'Failed to create appointment'
]);
}
}
public function addDoctor()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$specializationModel = new SpecializationModel();
return view('admin/add_doctor', [
'specializationOptions' => $specializationModel->getOptionNames(),
'isEdit' => false,
]);
}
public function addPatient($patientId = null)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
return view('admin/add_patient', [
'isEdit' => false,
'patient_id' => $patientId
]);
}
public function editDoctor($encryptedId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = decrypt_id($encryptedId);
if (! ctype_digit((string) $id)) {
return redirect()->to(site_url('admin/doctors'))->with('error', 'Invalid doctor link.');
}
$doctorData = $this->getDoctorFormData((int) $id);
if (! $doctorData) {
return redirect()->to(site_url('admin/doctors'))->with('error', 'Doctor not found.');
}
return view('admin/add_doctor', array_merge($doctorData, [
'isEdit' => true,
]));
}
public function editPatient($encryptedId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$id = decrypt_id($encryptedId);
if (! ctype_digit((string) $id)) {
return redirect()->to(site_url('admin/patients'))->with('error', 'Invalid patient link.');
}
$patientData = $this->getPatientFormData((int) $id);
if (! $patientData) {
return redirect()->to(site_url('admin/patients'))->with('error', 'Patient not found.');
}
return view('admin/add_patient', array_merge($patientData, [
'isEdit' => true,
]));
}
public function storeDoctor()
{
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]',
'gender' => 'required|in_list[Male,Female,Other]',
'experience_years' => 'permit_empty|integer|greater_than_equal_to[0]|less_than_equal_to[60]',
'experience_months' => 'permit_empty|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();
$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 = $this->request->getPost('experience_years');
$monthsRaw = $this->request->getPost('experience_months');
$years = ($yearsRaw === '' || $yearsRaw === null) ? 0 : (int) $yearsRaw;
$months = ($monthsRaw === '' || $monthsRaw === null) ? 0 : (int) $monthsRaw;
$experience = $this->buildExperienceString($years, $months);
$generatedPassword = $this->generateAccountPassword();
$db->transStart();
$userData = [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => trim((string) $this->request->getPost('email')),
'gender' => trim((string) $this->request->getPost('gender')),
'password' => password_hash($generatedPassword, PASSWORD_DEFAULT),
'role' => 'doctor',
'status' => 'active',
];
if (! $userModel->skipValidation(true)->insert($userData)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create doctor login account.');
}
$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,
];
if (! $doctorModel->skipValidation(true)->insert($doctorRow)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create doctor profile.');
}
$doctorId = (int) $doctorModel->getInsertID();
$specializationMap = $specializationModel->ensureNamesExist($specializations);
$specializationIds = array_values($specializationMap);
$doctorSpecializationModel->syncDoctorSpecializations($doctorId, $specializationIds, (int) session()->get('id'));
$db->transComplete();
if (! $db->transStatus()) {
return redirect()->back()->withInput()->with('error', 'Transaction failed.');
}
$logModel = new ActivityLogModel();
$logModel->log('create_doctor', "Admin created doctor {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/doctors'))->with('success', 'Doctor account created. Generated password: ' . $generatedPassword);
}
public function storePatient()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$rules = [
'first_name' => 'required|min_length[2]|max_length[50]|alpha_space',
'last_name' => 'required|min_length[2]|max_length[50]|alpha_space',
'email' => 'required|valid_email|is_unique[users.email]',
'phone' => 'required|min_length[10]|max_length[15]',
'dob' => 'permit_empty|valid_date[Y-m-d]',
'gender' => 'permit_empty|in_list[male,female,other]',
];
if (! $this->validate($rules)) {
return redirect()->back()->withInput();
}
if (! $this->validateDobInput()) {
return redirect()->back()->withInput()->with('error', 'Please enter a valid date of birth that is not in the future.');
}
$userModel = new UserModel();
$patientModel = new PatientModel();
$db = \Config\Database::connect();
$firstName = trim((string) $this->request->getPost('first_name'));
$lastName = trim((string) $this->request->getPost('last_name'));
$generatedPassword = $this->generateAccountPassword();
$dobRaw = trim((string) $this->request->getPost('dob'));
$db->transStart();
$userData = [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => trim((string) $this->request->getPost('email')),
'password' => password_hash($generatedPassword, PASSWORD_DEFAULT),
'role' => 'patient',
'status' => 'active',
];
if (! $userModel->skipValidation(true)->insert($userData)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create patient login account.');
}
$userId = (int) $userModel->getInsertID();
$patientRow = [
'user_id' => $userId,
'dob' => $dobRaw !== '' ? $dobRaw : null,
'gender' => $this->request->getPost('gender') !== '' ? $this->request->getPost('gender') : null,
'phone' => trim((string) $this->request->getPost('phone')),
];
if (! $patientModel->skipValidation(true)->insert($patientRow)) {
$db->transRollback();
return redirect()->back()->withInput()->with('error', 'Could not create patient profile.');
}
$db->transComplete();
if (! $db->transStatus()) {
return redirect()->back()->withInput()->with('error', 'Transaction failed.');
}
$logModel = new ActivityLogModel();
$logModel->log('create_patient', "Admin created patient {$firstName} {$lastName}", 'user', $userId);
return redirect()->to(site_url('admin/patients'))->with('success', 'Patient account created. Generated password: ' . $generatedPassword);
}
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 . ']',
'gender' => 'required|in_list[Male,Female,Other]',
'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 = $this->request->getPost('experience_years');
$monthsRaw = $this->request->getPost('experience_months');
$years = ($yearsRaw === '' || $yearsRaw === null) ? 0 : (int) $yearsRaw;
$months = ($monthsRaw === '' || $monthsRaw === null) ? 0 : (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')),
'gender' => trim((string) $this->request->getPost('gender')),
]);
$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[] = [
'doctor_id' => (int) ($doc->doctor_id ?? 0),
'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,
]);
}
public function getAvailableDoctorsForPatient()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
helper('encryption');
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(403);
}
$date = $this->request->getPost('date');
$time = $this->request->getPost('time');
$previewId = (int) $this->request->getPost('preview_id');
$specializationIds = $this->request->getPost('specialization_id');
if (is_string($specializationIds)) {
$decoded = json_decode($specializationIds, true);
if (is_array($decoded)) {
$specializationIds = $decoded;
} elseif ($specializationIds !== '') {
$specializationIds = preg_split('/\s*,\s*/', $specializationIds, -1, PREG_SPLIT_NO_EMPTY);
} else {
$specializationIds = [];
}
}
if (! is_array($specializationIds) || empty($specializationIds)) {
return $this->response->setJSON([
'success' => false,
'message' => 'No specialization selected',
'data' => [],
'doctors' => [],
]);
}
$specializationIds = array_values(array_filter(array_map('intval', $specializationIds), fn ($id) => $id > 0));
if (empty($specializationIds)) {
return $this->response->setJSON([
'success' => false,
'message' => 'No valid specialization selected',
'data' => [],
'doctors' => [],
]);
}
$dayNumber = null;
$requestedTime = $this->normalizeAppointmentTime((string) $time);
if (! empty($date)) {
try {
$dateObj = new \DateTime($date);
$dayNumber = (int) $dateObj->format('N');
} catch (\Exception $e) {
$dayNumber = null;
}
}
$db = \Config\Database::connect();
// Get doctors with that specialization
$builder = $db->table('doctors d');
$builder->select("\n d.id,\n d.user_id,\n d.experience,\n CONCAT(u.first_name,' ',u.last_name) as name,\n u.gender\n ");
$builder->join('users u', 'u.id=d.user_id');
$builder->join('doctor_specializations ds', 'ds.doctor_id=d.id');
$builder->whereIn('ds.specialization_id', $specializationIds);
$builder->groupBy(['d.id', 'd.user_id', 'd.experience', 'u.first_name', 'u.last_name', 'u.gender']);
$builder->orderBy('u.first_name', 'ASC');
$doctors = $builder->get()->getResultArray();
$appointmentModel = new AppointmentModel();
$bookingModel = new DoctorBookingModel();
$availableDoctors = [];
foreach ($doctors as $doctor) {
// Format doctor ID as PHY + 7-digit user_id (from users table)
$formattedDoctorId = null;
if (!empty($doctor['user_id'])) {
$formattedDoctorId = 'PHY' . str_pad($doctor['user_id'], 7, '0', STR_PAD_LEFT);
} else {
$formattedDoctorId = 'PHY' . str_pad($doctor['id'], 7, '0', STR_PAD_LEFT);
}
$doctor['formatted_doctor_id'] = $formattedDoctorId;
$doctor['doctor_edit_token'] = encrypt_id($doctor['user_id'] ?? 0);
$doctor['is_available'] = false;
$doctor['status'] = 'No Slots';
$doctor['time_slots'] = [];
$doctor['available_days'] = [];
$doctor['available_days_text'] = 'No availability';
$availableDayRows = $db->table('doctor_availability da')
->distinct()
->select('da.day_number, days.day_name')
->join('days', 'days.day_number = da.day_number', 'left')
->where('da.doctor_id', $doctor['id'])
->where('da.status', 1)
->orderBy('da.day_number', 'ASC')
->get()
->getResultArray();
foreach ($availableDayRows as $availableDay) {
$availableDayNumber = (int) ($availableDay['day_number'] ?? 0);
$dayName = trim((string) ($availableDay['day_name'] ?? ''));
if ($dayName === '') {
$dayName = [
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
7 => 'Sunday',
][$availableDayNumber] ?? '';
}
if ($dayName !== '') {
$doctor['available_days'][] = $dayName;
}
}
$doctor['available_days'] = array_values(array_unique($doctor['available_days']));
if ($doctor['available_days'] !== []) {
$doctor['available_days_text'] = implode(', ', $doctor['available_days']);
}
// Fetch ALL availability slots for this doctor across ALL days
$allSlotsBuilder = $db->table('doctor_availability da')
->select('da.start_time, da.end_time, da.day_number, days.day_name')
->join('days', 'days.day_number = da.day_number', 'left')
->where('da.doctor_id', $doctor['id'])
->where('da.status', 1)
->orderBy('da.day_number', 'ASC')
->orderBy('da.start_time', 'ASC');
$allSlots = $allSlotsBuilder->get()->getResultArray();
$doctor['all_slots'] = [];
foreach ($allSlots as $slot) {
$dayName = trim((string) ($slot['day_name'] ?? ''));
if ($dayName === '') {
$dayName = [
1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday',
4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday',
][$slot['day_number']] ?? 'Unknown';
}
$doctor['all_slots'][] = [
'day' => $dayName,
'start_time' => $slot['start_time'],
'end_time' => $slot['end_time'],
'start_time_formatted' => date('h:i A', strtotime($slot['start_time'])),
'end_time_formatted' => date('h:i A', strtotime($slot['end_time'])),
];
}
// Fetch availability slots for this doctor on the SPECIFIED day and time for booking status
if ($dayNumber !== null) {
$availabilityBuilder = $db->table('doctor_availability')
->where('doctor_id', $doctor['id'])
->where('day_number', $dayNumber)
->where('status', 1)
->orderBy('start_time', 'ASC');
if ($requestedTime !== null) {
$availabilityBuilder
->where('start_time <=', $requestedTime)
->where('end_time >', $requestedTime);
}
$availability = $availabilityBuilder->get()->getResultArray();
if ($availability) {
foreach ($availability as $slot) {
$doctor['time_slots'][] = [
'start_time' => $slot['start_time'],
'end_time' => $slot['end_time'],
'start_time_formatted' => date('h:i A', strtotime($slot['start_time'])),
'end_time_formatted' => date('h:i A', strtotime($slot['end_time'])),
];
}
}
}
$doctor['is_available'] = $doctor['time_slots'] !== [];
if ($doctor['is_available']) {
$doctor['status'] = 'Available';
} else {
$doctor['status'] = 'Not Available';
}
if ($doctor['is_available'] && $requestedTime !== null && ! empty($date)) {
if ($bookingModel->isDoctorBooked($doctor['id'], $date, $requestedTime)) {
$doctor['is_available'] = false;
$doctor['status'] = 'Booked';
}
}
$availableDoctors[] = $doctor;
}
// Fetch specializations for all available doctors
if (!empty($availableDoctors)) {
foreach ($availableDoctors as &$doc) {
$doc['specializations'] = $db->table('doctor_specializations ds')
->select('s.name, s.bg_color, s.text_color')
->join('specializations s', 's.id = ds.specialization_id')
->where('ds.doctor_id', $doc['id'])
->get()
->getResultArray();
}
}
if ($previewId > 0) {
$availableDaysSnapshot = [];
$availableSlotsSnapshot = [];
foreach ($availableDoctors as $doctor) {
$doctorId = (int) ($doctor['id'] ?? 0);
if ($doctorId < 1) {
continue;
}
$snapshotKey = (string) ($doctor['formatted_doctor_id'] ?? $doctorId);
$availableDaysSnapshot[$snapshotKey] = [
'doctor_id' => $doctorId,
'name' => $doctor['name'] ?? null,
'days' => $doctor['available_days'] ?? [],
];
$availableSlotsSnapshot[$snapshotKey] = [
'doctor_id' => $doctorId,
'name' => $doctor['name'] ?? null,
'slots' => $doctor['time_slots'] ?? [],
];
}
// ...existing code...
$previewModel->update($previewId, [
'available_days_snapshot' => $availableDaysSnapshot,
'available_slots_snapshot' => $availableSlotsSnapshot,
'status' => 'viewed',
]);
}
return $this->response->setJSON([
'success' => true,
'message' => 'Doctors loaded successfully',
'day_number' => $dayNumber,
'data' => $availableDoctors,
'doctors' => $availableDoctors,
]);
}
public function checkDoctorStatus()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$doctorId = (int) $this->request->getPost('doctor_id');
$date = $this->request->getPost('date');
$time = $this->request->getPost('time');
if ($doctorId <= 0 || empty($date) || empty($time)) {
return $this->response->setJSON(['status' => 'Available']);
}
$requestedTime = $this->normalizeAppointmentTime((string) $time);
$bookingModel = new DoctorBookingModel();
$isBooked = $bookingModel->isDoctorBooked($doctorId, $date, $requestedTime);
return $this->response->setJSON([
'status' => $isBooked ? 'Booked' : 'Available'
]);
}
public function getSpecializations()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(403);
}
$specializationModel = new SpecializationModel();
$specializations = $specializationModel->orderBy('name', 'ASC')->findAll();
return $this->response->setJSON([
'success' => true,
'specializations' => $specializations
]);
}
public function appointmentForm($patientId)
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$patientModel = new PatientModel();
$userModel = new UserModel();
$patient = $patientModel->findByUserId((int)$patientId);
if (!$patient) {
return redirect()
->to(site_url('admin/patients'))
->with('error','Patient not found');
}
$user = $userModel->find((int)$patientId);
$formattedPatientId =
(!empty($user['formatted_user_id']) &&
strpos($user['formatted_user_id'],'PHY') !== 0)
? $user['formatted_user_id']
: 'PT' . str_pad($user['id'],7,'0',STR_PAD_LEFT);
return view('admin/add_appointment',[
'patient' => $patient,
'user' => $user,
'formattedPatientId' => $formattedPatientId
]);
}
public function doctorSearch()
{
if ($r = $this->requireRole('admin')) {
return $r;
}
$doctorModel = new \App\Models\DoctorModel();
$availabilityModel = new \App\Models\AvailabilityModel();
$specializationModel = new \App\Models\SpecializationModel();
$specializations = $specializationModel->select('name')->orderBy('name', 'ASC')->findAll();
$doctors = $doctorModel
->select('doctors.id, doctors.user_id, doctors.specialization, doctors.experience, doctors.fees, users.first_name, users.last_name')
->join('users', 'users.id = doctors.user_id')
->findAll();
$dayNumberToNextDate = [];
for ($i = 1; $i <= 7; $i++) {
$todayDayNum = (int) date('N');
$daysAhead = ($i - $todayDayNum + 7) % 7;
$dayNumberToNextDate[$i] = date('Y-m-d', strtotime("+{$daysAhead} days"));
}
$doctorsData = [];
foreach ($doctors as $doctor) {
$db = \Config\Database::connect();
$doctorSpecializations = $db->table('doctor_specializations ds')
->select('s.name,s.bg_color,s.text_color')
->join('specializations s','s.id=ds.specialization_id')
->where('ds.doctor_id',$doctor['id'])
->get()
->getResultArray();
// Create fresh instance for each doctor to avoid query builder state issues
$availabilityModel = new \App\Models\AvailabilityModel();
$availability = $availabilityModel
->where('doctor_id', $doctor['id'])
->where('status', 1)
->orderBy('day_number', 'ASC')
->orderBy('start_time', 'ASC')
->findAll();
// Log for debugging
log_message('info', "Doctor: {$doctor['first_name']} {$doctor['last_name']} (ID: {$doctor['id']}) - Availability records: " . count($availability));
$timeSlots = [];
$availabilityByDate = [];
foreach ($availability as $avail) {
$dayNum = (int) $avail['day_number'];
// ✅ CORRECT: map the weekday number to the next real calendar date
$date = $dayNumberToNextDate[$dayNum]
?? date('Y-m-d', strtotime("+{$dayNum} days")); // fallback
// Human-readable label e.g. "Mon, Apr 28"
$dateLabel = date('D, M j', strtotime($date));
// Safely format the start time
$startTimeFormatted = false;
if (!empty($avail['start_time'])) {
$parsed = strtotime($avail['start_time']);
if ($parsed !== false && !empty($avail['end_time'])) {
$endParsed = strtotime($avail['end_time']);
if($endParsed !== false){
$timeSlots[] = [
'day' => date('D', strtotime($date)),
'start' => date('g:ia', $parsed),
'end' => date('g:ia', $endParsed)
];
}
}
}
if (!isset($availabilityByDate[$date])) {
$availabilityByDate[$date] = [
'label' => $dateLabel,
'day' => $dayNum,
'slots' => [],
];
}
// Only add slot if times were parsed successfully
if ($parsed !== false && !empty($avail['end_time'])) {
$endParsed = strtotime($avail['end_time']);
if ($endParsed !== false) {
$availabilityByDate[$date]['slots'][] = [
'start' => date('h:i A', $parsed),
'end' => date('h:i A', $endParsed),
'start_24' => date('H:i', $parsed),
'end_24' => date('H:i', $endParsed),
];
}
}
}
// Sort by date ascending so the nearest day shows first
ksort($availabilityByDate);
$doctorsData[] = [
'id' => $doctor['id'],
'first_name' => $doctor['first_name'],
'last_name' => $doctor['last_name'],
'specializations' => $doctorSpecializations,
'experience' => $doctor['experience'],
'fees' => $doctor['fees'],
'timeSlots' => array_slice($timeSlots, 0, 4),
'extraSlots' => array_slice($timeSlots, 4),
'availabilityByDate' => $availabilityByDate,
'hasAvailability' => !empty($availability), // explicit flag for the view
'avatar' => strtoupper(
substr($doctor['first_name'] ?? '', 0, 1) .
substr($doctor['last_name'] ?? '', 0, 1)
),
];
}
return view('admin/search_doctors', [
'doctors' => $doctorsData,
'specializations' => $specializations,
]);
}
public function appointmentsData()
{
$appointmentModel = new \App\Models\AppointmentModel();
$specializationModel = new \App\Models\SpecializationModel();
$appointments = $appointmentModel->getAdminAppointments(); // better than findAll()
$specializations = $specializationModel->findAll();
$serviceMap = [];
foreach ($specializations as $specialization) {
$serviceMap[(int) ($specialization['id'] ?? 0)] = trim((string) ($specialization['name'] ?? ''));
}
$data = [];
foreach ($appointments as $a) {
$serviceIds = json_decode((string) ($a->services ?? '[]'), true);
$serviceNames = [];
$doctorName = trim((string) ($a->doctor_name ?? ''));
if (is_array($serviceIds)) {
foreach ($serviceIds as $serviceId) {
$serviceId = (int) $serviceId;
if ($serviceId > 0 && ! empty($serviceMap[$serviceId])) {
$serviceNames[] = $serviceMap[$serviceId];
}
}
}
$data[] = [
'id' => $a->id,
'sl_no' => 'APT' . str_pad((string) ($a->id ?? 0), 7, '0', STR_PAD_LEFT),
'patient_id' => $a->patient_id,
'services' => $serviceIds,
'patient_name' => $a->patient_name,
'service_request' => $serviceNames !== [] ? implode(', ', array_unique($serviceNames)) : 'Not Specified',
'doctor_id' => (int) ($a->doctor_id ?? 0),
'doctor_name' => $doctorName !== '' ? $doctorName : 'Not Assigned',
'appointment_date' => $a->appointment_date,
'appointment_time' => $a->appointment_time,
'status' => $a->status ?: 'pending'
];
}
return $this->response->setJSON(['data' => $data]);
}
// ...existing code...
}