From 9a2dea55963a42e2c009977ef52146ea5d04b67e Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Mar 2026 08:22:39 +0000 Subject: [PATCH] Autosave: 20260317-082239 --- api/appointments.php | 161 ++++++++----- api/queue.php | 20 +- check_appointments_schema.php | 6 + check_data.php | 14 ++ check_doctor_holidays_table.php | 19 ++ check_nurses_schema.php | 6 + ...60317_add_show_in_queue_to_departments.sql | 1 + db/migrations/20260317_create_queue_ads.sql | 7 + db/migrations/20260317_enable_home_visits.sql | 11 + delete_test_appointment.php | 10 + doctor_holidays.php | 4 + home_visits.php | 10 + includes/actions.php | 105 ++++++--- includes/layout/header.php | 7 +- includes/pages/appointments.php | 171 +++++++++++++- includes/pages/departments.php | 25 +- includes/pages/doctor_holidays.php | 179 +++++++++++++++ includes/pages/home_visits.php | 217 ++++++++++++++++++ includes/pages/queue_ads.php | 200 ++++++++++++++++ includes/pages/visits.php | 65 +++++- lang.php | 48 ++++ queue_ads.php | 13 ++ queue_display.php | 65 +++++- test_api_appointments.php | 5 + test_create_appointment_curl.php | 28 +++ 25 files changed, 1286 insertions(+), 111 deletions(-) create mode 100644 check_appointments_schema.php create mode 100644 check_data.php create mode 100644 check_doctor_holidays_table.php create mode 100644 check_nurses_schema.php create mode 100644 db/migrations/20260317_add_show_in_queue_to_departments.sql create mode 100644 db/migrations/20260317_create_queue_ads.sql create mode 100644 db/migrations/20260317_enable_home_visits.sql create mode 100644 delete_test_appointment.php create mode 100644 doctor_holidays.php create mode 100644 home_visits.php create mode 100644 includes/pages/doctor_holidays.php create mode 100644 includes/pages/home_visits.php create mode 100644 includes/pages/queue_ads.php create mode 100644 queue_ads.php create mode 100644 test_api_appointments.php create mode 100644 test_create_appointment_curl.php diff --git a/api/appointments.php b/api/appointments.php index 9e1694d..43f2566 100644 --- a/api/appointments.php +++ b/api/appointments.php @@ -6,7 +6,7 @@ header('Content-Type: application/json'); $db = db(); $lang = $_SESSION['lang'] ?? 'en'; -$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +$method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { $id = $_GET['id'] ?? null; @@ -30,12 +30,14 @@ if ($method === 'GET') { $query = " SELECT a.id, a.start_time as start, a.end_time as end, a.reason, a.status, - a.patient_id, a.doctor_id, + a.patient_id, a.doctor_id, a.nurse_id, a.visit_type, a.address, p.name as patient_name, - d.name_$lang as doctor_name + d.name_$lang as doctor_name, + n.name_$lang as nurse_name FROM appointments a JOIN patients p ON a.patient_id = p.id - JOIN doctors d ON a.doctor_id = d.id + LEFT JOIN doctors d ON a.doctor_id = d.id + LEFT JOIN nurses n ON a.nurse_id = n.id WHERE 1=1"; $params = []; @@ -43,18 +45,30 @@ if ($method === 'GET') { if ($endStr) { $query .= " AND a.start_time <= ?"; $params[] = $endStr; } if ($doctor_id) { $query .= " AND a.doctor_id = ?"; $params[] = $doctor_id; } - $stmt = $db->prepare($query); - $stmt->execute($params); - $appointments = $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $stmt = $db->prepare($query); + $stmt->execute($params); + $appointments = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + error_log("Appointments Fetch Error: " . $e->getMessage()); + $appointments = []; + } foreach ($appointments as $a) { $color = '#0d6efd'; // blue if ($a['status'] === 'Completed') $color = '#198754'; // green if ($a['status'] === 'Cancelled') $color = '#dc3545'; // red + if ($a['visit_type'] === 'Home') $color = '#fd7e14'; // orange for home visits + $providerName = $a['doctor_name'] ? $a['doctor_name'] : ($a['nurse_name'] . ' (' . __('nurse') . ')'); + $title = $a['patient_name'] . ' - ' . $providerName; + if ($a['visit_type'] === 'Home') { + $title = '[🏠] ' . $title; + } + $events[] = [ 'id' => $a['id'], - 'title' => $a['patient_name'] . ' (' . $a['doctor_name'] . ')', + 'title' => $title, 'start' => $a['start'], 'end' => $a['end'], 'color' => $color, @@ -62,8 +76,12 @@ if ($method === 'GET') { 'type' => 'appointment', 'patient_id' => $a['patient_id'], 'doctor_id' => $a['doctor_id'], + 'nurse_id' => $a['nurse_id'], + 'visit_type' => $a['visit_type'], + 'address' => $a['address'], 'patient_name' => $a['patient_name'], 'doctor_name' => $a['doctor_name'], + 'nurse_name' => $a['nurse_name'], 'status' => $a['status'], 'reason' => $a['reason'] ] @@ -76,9 +94,17 @@ if ($method === 'GET') { if ($startStr) { $holidayQuery .= " AND holiday_date >= ?"; $holidayParams[] = date('Y-m-d', strtotime($startStr)); } if ($endStr) { $holidayQuery .= " AND holiday_date <= ?"; $holidayParams[] = date('Y-m-d', strtotime($endStr)); } - $stmt = $db->prepare($holidayQuery); - $stmt->execute($holidayParams); - $holidays = $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $stmt = $db->prepare($holidayQuery); + $stmt->execute($holidayParams); + $holidays = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + $holidays = []; + } + + $s = get_system_settings(); + $global_start = $s['working_hours_start'] ?? '08:00'; + $global_end = $s['working_hours_end'] ?? '17:00'; foreach ($holidays as $h) { // Render a visible block event in the time grid @@ -134,9 +160,13 @@ if ($method === 'GET') { $docHolidayParams[] = $doctor_id; } - $stmt = $db->prepare($docHolidayQuery); - $stmt->execute($docHolidayParams); - $docHolidays = $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $stmt = $db->prepare($docHolidayQuery); + $stmt->execute($docHolidayParams); + $docHolidays = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + $docHolidays = []; + } foreach ($docHolidays as $dh) { $title = $dh['doctor_name'] . ' - ' . ($dh['note'] ?: 'Holiday'); @@ -145,21 +175,12 @@ if ($method === 'GET') { $currentDate = strtotime($dh['start_date']); $endDate = strtotime($dh['end_date']); - // Output background events for each day individually to ensure they render in the time grid perfectly + $isFilteredDoctor = ($doctor_id && $doctor_id == $dh['doctor_id']); + + // Output background events for each day individually while ($currentDate <= $endDate) { $dateStr = date('Y-m-d', $currentDate); - // To block selection, FullCalendar relies on selectOverlap checking properties. - // We'll set 'overlap: false' when filtered to a specific doctor. - // Also we give it time 00:00:00 to 24:00:00 to fill the column. - - $isFilteredDoctor = ($doctor_id && $doctor_id == $dh['doctor_id']); - - // Render a visible block event in the time grid so it's super obvious - $s = get_system_settings(); - $global_start = $s['working_hours_start'] ?? '08:00'; - $global_end = $s['working_hours_end'] ?? '17:00'; - $events[] = [ 'id' => 'doc_hol_block_' . $dh['id'] . '_' . $dateStr, 'title' => 'Holiday: ' . $dh['doctor_name'], @@ -172,7 +193,6 @@ if ($method === 'GET') { 'extendedProps' => ['type' => 'doctor_holiday_block', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor] ]; - // Keep the background shading $events[] = [ 'id' => 'doc_hol_bg_' . $dh['id'] . '_' . $dateStr, 'start' => $dateStr . 'T00:00:00', @@ -181,13 +201,12 @@ if ($method === 'GET') { 'allDay' => false, 'backgroundColor' => $isFilteredDoctor ? 'rgba(255, 193, 7, 0.5)' : 'rgba(255, 193, 7, 0.15)', 'overlap' => !$isFilteredDoctor, - 'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor, 'blocks_selection' => $isFilteredDoctor] + 'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor] ]; $currentDate = strtotime('+1 day', $currentDate); } - // Visible event strip across the top (allDay) $events[] = [ 'id' => 'doc_hol_' . $dh['id'], 'title' => $title, @@ -220,7 +239,6 @@ if ($method === 'GET') { } $businessHours = array_values($bhMap); } else { - $s = get_system_settings(); $st = $s['working_hours_start'] ?? '08:00'; $et = $s['working_hours_end'] ?? '17:00'; $businessHours = [ @@ -232,10 +250,6 @@ if ($method === 'GET') { ]; } - $s = get_system_settings(); - $global_start = $s['working_hours_start'] ?? '08:00'; - $global_end = $s['working_hours_end'] ?? '17:00'; - echo json_encode([ 'events' => $events, 'businessHours' => $businessHours, @@ -250,64 +264,103 @@ if ($method === 'GET') { function checkDoctorHoliday($db, $doctor_id, $start_time) { if (!$doctor_id || !$start_time) return false; $date = date('Y-m-d', strtotime($start_time)); - $stmt = $db->prepare("SELECT COUNT(*) FROM doctor_holidays WHERE doctor_id = ? AND ? BETWEEN start_date AND end_date"); - $stmt->execute([$doctor_id, $date]); - return $stmt->fetchColumn() > 0; + try { + $stmt = $db->prepare("SELECT COUNT(*) FROM doctor_holidays WHERE doctor_id = ? AND ? BETWEEN start_date AND end_date"); + $stmt->execute([$doctor_id, $date]); + return $stmt->fetchColumn() > 0; + } catch (PDOException $e) { + error_log("Check Holiday Error: " . $e->getMessage()); + return false; + } } if ($method === 'POST') { - $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; + $input = json_decode(file_get_contents('php://input'), true); + if (!$input) { + $input = $_POST; + } + + if (empty($input)) { + echo json_encode(['success' => false, 'error' => 'No input data received']); + exit; + } + $action = $input['action'] ?? ''; + if (empty($action)) { + echo json_encode(['success' => false, 'error' => 'No action specified']); + exit; + } + if ($action === 'create') { $patient_id = $input['patient_id'] ?? ''; - $doctor_id = $input['doctor_id'] ?? ''; + $doctor_id = $input['doctor_id'] ?: null; // Nullable + $nurse_id = $input['nurse_id'] ?: null; // Nullable + $visit_type = $input['visit_type'] ?? 'Clinic'; + $address = $input['address'] ?? ''; $start_time = $input['start_time'] ?? ''; $reason = $input['reason'] ?? ''; - if ($patient_id && $doctor_id && $start_time) { - // Check for holiday conflict - if (checkDoctorHoliday($db, $doctor_id, $start_time)) { + if ($patient_id && ($doctor_id || $nurse_id) && $start_time) { + // Check for holiday conflict if doctor assigned + if ($doctor_id && checkDoctorHoliday($db, $doctor_id, $start_time)) { echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']); exit; } - $stmt = $db->prepare("INSERT INTO appointments (patient_id, doctor_id, start_time, end_time, reason) VALUES (?, ?, ?, DATE_ADD(?, INTERVAL 30 MINUTE), ?)"); - $stmt->execute([$patient_id, $doctor_id, $start_time, $start_time, $reason]); - echo json_encode(['success' => true, 'id' => $db->lastInsertId()]); + try { + $stmt = $db->prepare("INSERT INTO appointments (patient_id, doctor_id, nurse_id, visit_type, address, start_time, end_time, reason) VALUES (?, ?, ?, ?, ?, ?, DATE_ADD(?, INTERVAL 30 MINUTE), ?)"); + $stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $start_time, $start_time, $reason]); + echo json_encode(['success' => true, 'id' => $db->lastInsertId()]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]); + } } else { echo json_encode(['success' => false, 'error' => 'Missing fields']); } } elseif ($action === 'update') { $id = $input['id'] ?? ''; $patient_id = $input['patient_id'] ?? ''; - $doctor_id = $input['doctor_id'] ?? ''; + $doctor_id = $input['doctor_id'] ?: null; + $nurse_id = $input['nurse_id'] ?: null; + $visit_type = $input['visit_type'] ?? 'Clinic'; + $address = $input['address'] ?? ''; $start_time = $input['start_time'] ?? ''; $status = $input['status'] ?? 'Scheduled'; $reason = $input['reason'] ?? ''; - if ($id && $patient_id && $doctor_id && $start_time) { + if ($id && $patient_id && ($doctor_id || $nurse_id) && $start_time) { // Check for holiday conflict - if (checkDoctorHoliday($db, $doctor_id, $start_time)) { + if ($doctor_id && checkDoctorHoliday($db, $doctor_id, $start_time)) { echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']); exit; } - $stmt = $db->prepare("UPDATE appointments SET patient_id = ?, doctor_id = ?, start_time = ?, end_time = DATE_ADD(?, INTERVAL 30 MINUTE), status = ?, reason = ? WHERE id = ?"); - $stmt->execute([$patient_id, $doctor_id, $start_time, $start_time, $status, $reason, $id]); - echo json_encode(['success' => true]); + try { + $stmt = $db->prepare("UPDATE appointments SET patient_id = ?, doctor_id = ?, nurse_id = ?, visit_type = ?, address = ?, start_time = ?, end_time = DATE_ADD(?, INTERVAL 30 MINUTE), status = ?, reason = ? WHERE id = ?"); + $stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $start_time, $start_time, $status, $reason, $id]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]); + } } else { echo json_encode(['success' => false, 'error' => 'Missing fields']); } } elseif ($action === 'delete') { $id = $input['id'] ?? ''; if ($id) { - $stmt = $db->prepare("DELETE FROM appointments WHERE id = ?"); - $stmt->execute([$id]); - echo json_encode(['success' => true]); + try { + $stmt = $db->prepare("DELETE FROM appointments WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]); + } } else { echo json_encode(['success' => false, 'error' => 'Missing ID']); } + } else { + echo json_encode(['success' => false, 'error' => 'Invalid action']); } exit; } \ No newline at end of file diff --git a/api/queue.php b/api/queue.php index 2054796..3f91da1 100644 --- a/api/queue.php +++ b/api/queue.php @@ -171,9 +171,27 @@ try { exit; } + // --- GET ADS --- + if ($action === 'get_ads') { + $stmt = $db->query("SELECT * FROM queue_ads WHERE active = 1 ORDER BY created_at DESC"); + $ads = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Return both languages + $data = array_map(function($ad) { + return [ + 'id' => $ad['id'], + 'text_en' => $ad['text_en'], + 'text_ar' => $ad['text_ar'] + ]; + }, $ads); + + echo json_encode(['success' => true, 'data' => $data]); + exit; + } + throw new Exception('Invalid action'); } catch (Exception $e) { http_response_code(400); echo json_encode(['success' => false, 'error' => $e->getMessage()]); -} +} \ No newline at end of file diff --git a/check_appointments_schema.php b/check_appointments_schema.php new file mode 100644 index 0000000..e600f56 --- /dev/null +++ b/check_appointments_schema.php @@ -0,0 +1,6 @@ +query("DESCRIBE appointments"); +$columns = $stmt->fetchAll(PDO::FETCH_COLUMN); +print_r($columns); diff --git a/check_data.php b/check_data.php new file mode 100644 index 0000000..8126ab5 --- /dev/null +++ b/check_data.php @@ -0,0 +1,14 @@ +query("SELECT id, name FROM patients LIMIT 1")->fetch(PDO::FETCH_ASSOC); + $doctor = $db->query("SELECT id, name_en FROM doctors LIMIT 1")->fetch(PDO::FETCH_ASSOC); + + echo "Patient: " . ($patient ? "ID=" . $patient['id'] . ", Name=" . $patient['name'] : "None") . "\n"; + echo "Doctor: " . ($doctor ? "ID=" . $doctor['id'] . ", Name=" . $doctor['name_en'] : "None") . "\n"; +} catch (PDOException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + diff --git a/check_doctor_holidays_table.php b/check_doctor_holidays_table.php new file mode 100644 index 0000000..fa4fd71 --- /dev/null +++ b/check_doctor_holidays_table.php @@ -0,0 +1,19 @@ +query("SHOW COLUMNS FROM doctor_holidays"); + if ($result) { + $columns = $result->fetchAll(PDO::FETCH_ASSOC); + echo "Table 'doctor_holidays' exists with columns:\n"; + foreach ($columns as $col) { + echo "- " . $col['Field'] . " (" . $col['Type'] . ")\n"; + } + } else { + echo "Table 'doctor_holidays' does not exist.\n"; + } +} catch (PDOException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + diff --git a/check_nurses_schema.php b/check_nurses_schema.php new file mode 100644 index 0000000..0b48c62 --- /dev/null +++ b/check_nurses_schema.php @@ -0,0 +1,6 @@ +query("DESCRIBE nurses"); +$columns = $stmt->fetchAll(PDO::FETCH_COLUMN); +print_r($columns); diff --git a/db/migrations/20260317_add_show_in_queue_to_departments.sql b/db/migrations/20260317_add_show_in_queue_to_departments.sql new file mode 100644 index 0000000..e4652b5 --- /dev/null +++ b/db/migrations/20260317_add_show_in_queue_to_departments.sql @@ -0,0 +1 @@ +ALTER TABLE departments ADD COLUMN show_in_queue BOOLEAN DEFAULT 1; diff --git a/db/migrations/20260317_create_queue_ads.sql b/db/migrations/20260317_create_queue_ads.sql new file mode 100644 index 0000000..ae44abb --- /dev/null +++ b/db/migrations/20260317_create_queue_ads.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS queue_ads ( + id INT AUTO_INCREMENT PRIMARY KEY, + text_en TEXT NOT NULL, + text_ar TEXT NOT NULL, + active TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/db/migrations/20260317_enable_home_visits.sql b/db/migrations/20260317_enable_home_visits.sql new file mode 100644 index 0000000..4408dc9 --- /dev/null +++ b/db/migrations/20260317_enable_home_visits.sql @@ -0,0 +1,11 @@ +-- Add columns to appointments table +ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_id INT NULL; +ALTER TABLE appointments ADD COLUMN IF NOT EXISTS visit_type ENUM('Clinic', 'Home') DEFAULT 'Clinic'; +ALTER TABLE appointments ADD COLUMN IF NOT EXISTS address TEXT NULL; +ALTER TABLE appointments ADD CONSTRAINT fk_appointment_nurse FOREIGN KEY (nurse_id) REFERENCES nurses(id) ON DELETE SET NULL; + +-- Add columns to visits table +ALTER TABLE visits ADD COLUMN IF NOT EXISTS nurse_id INT NULL; +ALTER TABLE visits ADD COLUMN IF NOT EXISTS visit_type ENUM('Clinic', 'Home') DEFAULT 'Clinic'; +ALTER TABLE visits ADD COLUMN IF NOT EXISTS address TEXT NULL; +ALTER TABLE visits ADD CONSTRAINT fk_visit_nurse FOREIGN KEY (nurse_id) REFERENCES nurses(id) ON DELETE SET NULL; diff --git a/delete_test_appointment.php b/delete_test_appointment.php new file mode 100644 index 0000000..1fd7574 --- /dev/null +++ b/delete_test_appointment.php @@ -0,0 +1,10 @@ +prepare("DELETE FROM appointments WHERE id = ? AND reason = 'Test API'"); +$stmt->execute([$id]); +echo "Deleted test appointment $id (if it matched).\n"; + diff --git a/doctor_holidays.php b/doctor_holidays.php new file mode 100644 index 0000000..89e814b --- /dev/null +++ b/doctor_holidays.php @@ -0,0 +1,4 @@ + diff --git a/includes/actions.php b/includes/actions.php index 97bbfd6..adc4699 100644 --- a/includes/actions.php +++ b/includes/actions.php @@ -223,9 +223,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } elseif ($_POST['action'] === 'add_department') { $name_en = $_POST['name_en'] ?? ''; $name_ar = $_POST['name_ar'] ?? ''; + $show_in_queue = isset($_POST['show_in_queue']) ? 1 : 0; if ($name_en && $name_ar) { - $stmt = $db->prepare("INSERT INTO departments (name_en, name_ar) VALUES (?, ?)"); - $stmt->execute([$name_en, $name_ar]); + $stmt = $db->prepare("INSERT INTO departments (name_en, name_ar, show_in_queue) VALUES (?, ?, ?)"); + $stmt->execute([$name_en, $name_ar, $show_in_queue]); $_SESSION['flash_message'] = __('add_department') . ' ' . __('successfully'); $redirect = true; } @@ -233,9 +234,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $id = $_POST['id'] ?? ''; $name_en = $_POST['name_en'] ?? ''; $name_ar = $_POST['name_ar'] ?? ''; + $show_in_queue = isset($_POST['show_in_queue']) ? 1 : 0; if ($id && $name_en && $name_ar) { - $stmt = $db->prepare("UPDATE departments SET name_en = ?, name_ar = ? WHERE id = ?"); - $stmt->execute([$name_en, $name_ar, $id]); + $stmt = $db->prepare("UPDATE departments SET name_en = ?, name_ar = ?, show_in_queue = ? WHERE id = ?"); + $stmt->execute([$name_en, $name_ar, $show_in_queue, $id]); $_SESSION['flash_message'] = __('edit_department') . ' ' . __('successfully'); $redirect = true; } @@ -288,7 +290,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } elseif ($_POST['action'] === 'record_visit') { $patient_id = $_POST['patient_id'] ?? ''; - $doctor_id = $_POST['doctor_id'] ?? ''; + $doctor_id = $_POST['doctor_id'] ?: null; // Nullable + $nurse_id = $_POST['nurse_id'] ?: null; // Nullable + $visit_type = $_POST['visit_type'] ?? 'Clinic'; $appointment_id = $_POST['appointment_id'] ?: null; $weight = $_POST['weight'] ?? ''; $bp = $_POST['blood_pressure'] ?? ''; @@ -298,15 +302,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $diagnosis = $_POST['diagnosis'] ?? ''; $treatment = $_POST['treatment_plan'] ?? ''; - if ($patient_id && $doctor_id) { + if ($patient_id && ($doctor_id || $nurse_id)) { $db->beginTransaction(); - $stmt = $db->prepare("INSERT INTO visits (patient_id, doctor_id, appointment_id, weight, blood_pressure, heart_rate, temperature, symptoms, diagnosis, treatment_plan) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - $stmt->execute([$patient_id, $doctor_id, $appointment_id, $weight, $bp, $hr, $temp, $symptoms, $diagnosis, $treatment]); + + // Fetch address from appointment if not provided via some other means + // For now, we rely on appointment_id if present to get the address + $address = null; + if ($appointment_id) { + $stmtApt = $db->prepare("SELECT address FROM appointments WHERE id = ?"); + $stmtApt->execute([$appointment_id]); + $apt = $stmtApt->fetch(); + $address = $apt['address'] ?? null; + } + + $stmt = $db->prepare("INSERT INTO visits (patient_id, doctor_id, nurse_id, visit_type, address, appointment_id, weight, blood_pressure, heart_rate, temperature, symptoms, diagnosis, treatment_plan) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $appointment_id, $weight, $bp, $hr, $temp, $symptoms, $diagnosis, $treatment]); $visit_id = $db->lastInsertId(); $token_message = ''; - // Token Generation - if (isset($_POST['generate_token']) && $_POST['generate_token'] == '1') { + // Token Generation (Only for Doctor visits in Clinic usually) + if (isset($_POST['generate_token']) && $_POST['generate_token'] == '1' && $doctor_id) { $stmtDoc = $db->prepare("SELECT department_id FROM doctors WHERE id = ?"); $stmtDoc->execute([$doctor_id]); $docData = $stmtDoc->fetch(); @@ -341,6 +356,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = $db->prepare("UPDATE appointments SET status = 'Completed' WHERE id = ?"); $stmt->execute([$appointment_id]); } + + // Auto-create bill if requested (e.g. from Home Visits) + if (isset($_POST['create_bill']) && $_POST['create_bill'] == '1') { + $stmtIns = $db->prepare("SELECT insurance_company_id FROM patients WHERE id = ?"); + $stmtIns->execute([$patient_id]); + $patient = $stmtIns->fetch(); + + $total = 0; + $insurance_covered = 0; + $patient_payable = 0; + + $stmtBill = $db->prepare("INSERT INTO bills (patient_id, visit_id, total_amount, insurance_covered, patient_payable, status) VALUES (?, ?, ?, ?, ?, 'Pending')"); + $stmtBill->execute([$patient_id, $visit_id, $total, $insurance_covered, $patient_payable]); + $bill_id = $db->lastInsertId(); + + $stmtItem = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)"); + $stmtItem->execute([$bill_id, 'Home Visit Service', 0]); + } + $db->commit(); $_SESSION['flash_message'] = __('add_visit') . ' ' . __('successfully') . $token_message; $redirect = true; @@ -990,24 +1024,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $supplierMap[$supplier_name] = $supplier_id; } } - - /* - if ($expiry) { - if (is_numeric($expiry)) { - $ts = strtotime($expiry); - } else { - $ts = strtotime($expiry); - } - - if ($ts) { - $expiry = date('Y-m-d', $ts); - } else { - $expiry = null; - } - } else { - $expiry = null; - } - */ $stmt->execute([$name_en, $name_ar, $group_id, $price, $expiry, $supplier_id]); } @@ -1083,6 +1099,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $redirect = true; } } + } elseif ($_POST['action'] === 'add_queue_ad') { + $text_en = $_POST['text_en'] ?? ''; + $text_ar = $_POST['text_ar'] ?? ''; + $active = isset($_POST['active']) ? 1 : 0; + + if ($text_en && $text_ar) { + $stmt = $db->prepare("INSERT INTO queue_ads (text_en, text_ar, active) VALUES (?, ?, ?)"); + $stmt->execute([$text_en, $text_ar, $active]); + $_SESSION['flash_message'] = __('add_ad') . ' ' . __('successfully'); + $redirect = true; + } + } elseif ($_POST['action'] === 'edit_queue_ad') { + $id = $_POST['id'] ?? ''; + $text_en = $_POST['text_en'] ?? ''; + $text_ar = $_POST['text_ar'] ?? ''; + $active = isset($_POST['active']) ? 1 : 0; + + if ($id && $text_en && $text_ar) { + $stmt = $db->prepare("UPDATE queue_ads SET text_en = ?, text_ar = ?, active = ? WHERE id = ?"); + $stmt->execute([$text_en, $text_ar, $active, $id]); + $_SESSION['flash_message'] = __('edit_ad') . ' ' . __('successfully'); + $redirect = true; + } + } elseif ($_POST['action'] === 'delete_queue_ad') { + $id = $_POST['id'] ?? ''; + if ($id) { + $stmt = $db->prepare("DELETE FROM queue_ads WHERE id = ?"); + $stmt->execute([$id]); + $_SESSION['flash_message'] = __('delete') . ' ' . __('successfully'); + $redirect = true; + } } } @@ -1090,4 +1137,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { header("Location: " . $_SERVER['REQUEST_URI']); exit; } -} +} \ No newline at end of file diff --git a/includes/layout/header.php b/includes/layout/header.php index 9c7674d..33bf2a8 100644 --- a/includes/layout/header.php +++ b/includes/layout/header.php @@ -102,6 +102,7 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp "> + @@ -145,13 +146,14 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp + - + -
+
diff --git a/includes/pages/appointments.php b/includes/pages/appointments.php index db961f8..5e42c70 100644 --- a/includes/pages/appointments.php +++ b/includes/pages/appointments.php @@ -41,6 +41,10 @@
 
Cancelled
+
+
 
+ Home Visit +
 
Holiday @@ -71,18 +75,53 @@
-
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
@@ -134,19 +173,68 @@ function initAptSelect2() { } } +function toggleAddressField() { + var type = document.getElementById('apt_visit_type').value; + var div = document.getElementById('div_address'); + if (type === 'Home') { + div.classList.remove('d-none'); + // Auto-fill address if empty and patient selected + var addressField = document.getElementById('apt_address'); + if (!addressField.value) { + var patientSelect = document.getElementById('apt_patient_id'); + var selectedOption = patientSelect.options[patientSelect.selectedIndex]; + if (selectedOption && selectedOption.dataset.address) { + addressField.value = selectedOption.dataset.address; + } + } + } else { + div.classList.add('d-none'); + } +} + +function toggleProviderField() { + var type = document.getElementById('apt_provider_type').value; + var divDoc = document.getElementById('div_doctor'); + var divNurse = document.getElementById('div_nurse'); + + if (type === 'Nurse') { + divDoc.classList.add('d-none'); + divNurse.classList.remove('d-none'); + // Clear doctor selection? Maybe not necessary, backend handles nulls based on logic + } else { + divDoc.classList.remove('d-none'); + divNurse.classList.add('d-none'); + } + // Re-validate holiday check since provider changed + validateHolidayFrontend(); +} + function showCreateModal(startTime = null) { document.getElementById('modalTitle').innerText = ''; document.getElementById('apt_id').value = ''; document.getElementById('apt_reason').value = ''; document.getElementById('apt_status').value = 'Scheduled'; + + // Defaults + document.getElementById('apt_visit_type').value = 'Clinic'; + document.getElementById('apt_provider_type').value = 'Doctor'; + document.getElementById('apt_address').value = ''; + $('#apt_nurse_id').val('').trigger('change'); + + toggleAddressField(); + toggleProviderField(); + document.getElementById('btnDeleteApt').style.display = 'none'; - if (startTime) { + if (startTime && startTime instanceof Date) { var offset = startTime.getTimezoneOffset() * 60000; var localISOTime = (new Date(startTime.getTime() - offset)).toISOString().slice(0, 16); document.getElementById('apt_start_time').value = localISOTime; } else { - document.getElementById('apt_start_time').value = new Date().toISOString().slice(0, 16); + var now = new Date(); + var offset = now.getTimezoneOffset() * 60000; + var localISOTime = (new Date(now.getTime() - offset)).toISOString().slice(0, 16); + document.getElementById('apt_start_time').value = localISOTime; } if (document.getElementById('doctorFilter').value) { @@ -161,11 +249,16 @@ function showCreateModal(startTime = null) { function saveAppointment() { var id = document.getElementById('apt_id').value; + var providerType = document.getElementById('apt_provider_type').value; + var data = { action: id ? 'update' : 'create', id: id, patient_id: document.getElementById('apt_patient_id').value, - doctor_id: document.getElementById('apt_doctor_id').value, + doctor_id: providerType === 'Doctor' ? document.getElementById('apt_doctor_id').value : null, + nurse_id: providerType === 'Nurse' ? document.getElementById('apt_nurse_id').value : null, + visit_type: document.getElementById('apt_visit_type').value, + address: document.getElementById('apt_address').value, start_time: document.getElementById('apt_start_time').value, status: document.getElementById('apt_status').value, reason: document.getElementById('apt_reason').value @@ -176,7 +269,12 @@ function saveAppointment() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) .then(result => { if (result.success) { var modalEl = document.getElementById('appointmentDetailsModal'); @@ -186,6 +284,10 @@ function saveAppointment() { } else { alert('Error: ' + (result.error || 'Unknown error')); } + }) + .catch(error => { + console.error('Fetch error:', error); + alert('An error occurred while saving the appointment. Please check console for details.'); }); } @@ -198,7 +300,12 @@ function deleteAppointment() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', id: id }) }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) .then(result => { if (result.success) { var modalEl = document.getElementById('appointmentDetailsModal'); @@ -208,6 +315,10 @@ function deleteAppointment() { } else { alert('Error: ' + (result.error || 'Unknown error')); } + }) + .catch(error => { + console.error('Fetch error:', error); + alert('An error occurred while deleting the appointment. Please check console for details.'); }); } @@ -241,7 +352,12 @@ document.addEventListener('DOMContentLoaded', function() { businessHours: true, events: function(fetchInfo, successCallback, failureCallback) { fetch('api/appointments.php?start=' + fetchInfo.startStr + '&end=' + fetchInfo.endStr + '&doctor_id=' + doctorFilter.value) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) .then(data => { // We DO NOT setOption('businessHours') here to prevent FullCalendar from re-rendering and clearing events. successCallback(data.events); @@ -270,7 +386,22 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('modalTitle').innerText = 'Edit Appointment'; document.getElementById('apt_id').value = info.event.id; $('#apt_patient_id').val(props.patient_id).trigger('change'); - $('#apt_doctor_id').val(props.doctor_id).trigger('change'); + + // Set fields + var visitType = props.visit_type || 'Clinic'; + document.getElementById('apt_visit_type').value = visitType; + document.getElementById('apt_address').value = props.address || ''; + toggleAddressField(); + + var providerType = props.nurse_id ? 'Nurse' : 'Doctor'; + document.getElementById('apt_provider_type').value = providerType; + toggleProviderField(); + + if (providerType === 'Doctor') { + $('#apt_doctor_id').val(props.doctor_id).trigger('change'); + } else { + $('#apt_nurse_id').val(props.nurse_id).trigger('change'); + } var start = info.event.start; var offset = start.getTimezoneOffset() * 60000; @@ -299,11 +430,33 @@ document.addEventListener('DOMContentLoaded', function() { initAptSelect2(); validateHolidayFrontend(); }); + + // When patient changes, auto-fill address if empty and Home Visit is selected + $('#apt_patient_id').on('change', function() { + if (document.getElementById('apt_visit_type').value === 'Home') { + var addressField = document.getElementById('apt_address'); + if (!addressField.value) { + var selectedOption = this.options[this.selectedIndex]; + if (selectedOption && selectedOption.dataset.address) { + addressField.value = selectedOption.dataset.address; + } + } + } + }); $('#apt_doctor_id').on('change', validateHolidayFrontend); $('#apt_start_time').on('change', validateHolidayFrontend); + $('#apt_provider_type').on('change', validateHolidayFrontend); // Re-validate when provider changes function validateHolidayFrontend() { + var providerType = $('#apt_provider_type').val(); + if (providerType === 'Nurse') { + // Nurses don't have holiday checks yet + $('#holidayWarning').remove(); + document.getElementById('btnSaveApt').disabled = false; + return; + } + var docId = $('#apt_doctor_id').val(); var startTimeStr = $('#apt_start_time').val(); var btnSave = document.getElementById('btnSaveApt'); diff --git a/includes/pages/departments.php b/includes/pages/departments.php index bdb3085..18f19f6 100644 --- a/includes/pages/departments.php +++ b/includes/pages/departments.php @@ -32,7 +32,7 @@ if (isset($_GET['ajax_search'])) { if (empty($departments)): ?> - + @@ -43,6 +43,13 @@ if (isset($_GET['ajax_search'])) { + + + + + + +
+
+ + +
+ + + diff --git a/test_api_appointments.php b/test_api_appointments.php new file mode 100644 index 0000000..2f242d3 --- /dev/null +++ b/test_api_appointments.php @@ -0,0 +1,5 @@ + 'create', + 'patient_id' => 2, + 'doctor_id' => 1, + 'nurse_id' => null, + 'visit_type' => 'Clinic', + 'address' => '', + 'start_time' => '2026-03-25T10:00', + 'reason' => 'Test API' +]; + +$ch = curl_init('http://localhost/api/appointments.php'); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); +curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + +$response = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + +if ($response === false) { + echo "Curl Error: " . curl_error($ch); +} else { + echo "HTTP Code: $http_code\n"; + echo "Response: $response\n"; +} +curl_close($ch); \ No newline at end of file