diff --git a/api/appointments.php b/api/appointments.php index 874b712..d2e9529 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']; +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; if ($method === 'GET') { $id = $_GET['id'] ?? null; @@ -81,16 +81,26 @@ if ($method === 'GET') { $holidays = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($holidays as $h) { + // Render daily blocks for time grid $events[] = [ - 'id' => 'hol_' . $h['start'], - 'title' => __('holiday') . ': ' . $h['title'], + 'id' => 'hol_bg_' . $h['start'], 'start' => $h['start'] . 'T00:00:00', 'end' => $h['start'] . 'T23:59:59', - 'allDay' => false, // Set to false to cover vertical time slots + 'allDay' => false, 'display' => 'background', - 'backgroundColor' => '#ffc107', // Explicitly set background color - 'color' => '#ffc107', + 'backgroundColor' => 'rgba(255, 193, 7, 0.5)', 'overlap' => false, + 'extendedProps' => ['type' => 'public_holiday', 'blocks_selection' => true] + ]; + // Visible event strip across the top + $events[] = [ + 'id' => 'hol_title_' . $h['start'], + 'title' => __('holiday') . ': ' . $h['title'], + 'start' => $h['start'], + 'end' => $h['start'], + 'allDay' => true, + 'color' => '#ffc107', + 'textColor' => '#000', 'extendedProps' => ['type' => 'public_holiday'] ]; } @@ -117,34 +127,46 @@ if ($method === 'GET') { foreach ($docHolidays as $dh) { $title = $dh['doctor_name'] . ' - ' . ($dh['note'] ?: 'Holiday'); + $endDayStr = date('Y-m-d', strtotime($dh['end_date'] . ' +1 day')); - // If filtering by specific doctor, show as background to block the day clearly - // If showing all doctors, show as a block event so we know WHO is on holiday - if ($doctor_id) { + $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 + 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']); + $events[] = [ - 'id' => 'doc_hol_' . $dh['id'], - 'title' => 'Holiday', - 'start' => $dh['start_date'] . 'T00:00:00', - 'end' => date('Y-m-d', strtotime($dh['end_date'] . ' +1 day')) . 'T00:00:00', - 'allDay' => false, // Set to false to cover vertical time slots + 'id' => 'doc_hol_bg_' . $dh['id'] . '_' . $dateStr, + 'start' => $dateStr . 'T00:00:00', + 'end' => $dateStr . 'T23:59:59', 'display' => 'background', - 'backgroundColor' => '#ffc107', // Explicitly set background color - 'color' => '#ffc107', - 'overlap' => false, // Prevent booking on holiday - 'extendedProps' => ['type' => 'doctor_holiday'] - ]; - } else { - $events[] = [ - 'id' => 'doc_hol_' . $dh['id'], - 'title' => $title, - 'start' => $dh['start_date'], - 'end' => date('Y-m-d', strtotime($dh['end_date'] . ' +1 day')), - 'allDay' => true, - 'color' => '#fd7e14', // Orange - 'textColor' => '#fff', - 'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id']] + '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] ]; + + $currentDate = strtotime('+1 day', $currentDate); } + + // Visible event strip across the top (allDay) + $events[] = [ + 'id' => 'doc_hol_' . $dh['id'], + 'title' => $title, + 'start' => $dh['start_date'], + 'end' => $endDayStr, + 'allDay' => true, + 'color' => '#ffc107', + 'textColor' => '#000', + 'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id']] + ]; } // Fetch Doctor Business Hours @@ -167,22 +189,41 @@ 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 = [ [ 'daysOfWeek' => [0, 1, 2, 3, 4, 5, 6], - 'startTime' => '08:00', - 'endTime' => '17:00' + 'startTime' => $st, + 'endTime' => $et ] ]; } + + $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 + 'businessHours' => $businessHours, + 'settings' => [ + 'working_hours_start' => $global_start, + 'working_hours_end' => $global_end + ] ]); exit; } +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; +} + if ($method === 'POST') { $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $action = $input['action'] ?? ''; @@ -194,6 +235,12 @@ if ($method === 'POST') { $reason = $input['reason'] ?? ''; if ($patient_id && $doctor_id && $start_time) { + // Check for holiday conflict + if (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()]); @@ -209,6 +256,12 @@ if ($method === 'POST') { $reason = $input['reason'] ?? ''; if ($id && $patient_id && $doctor_id && $start_time) { + // Check for holiday conflict + if (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]); @@ -226,4 +279,4 @@ if ($method === 'POST') { } } exit; -} +} \ No newline at end of file diff --git a/helpers.php b/helpers.php index 6821ac0..6deb90a 100644 --- a/helpers.php +++ b/helpers.php @@ -1,4 +1,33 @@ query('SELECT setting_key, setting_value FROM settings'); + $settings = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $settings[$row['setting_key']] = $row['setting_value']; + } + return $settings; + } catch (Exception $e) { + return []; + } +} +function apply_timezone() { + $s = get_system_settings(); + if (!empty($s['timezone'])) { + date_default_timezone_set($s['timezone']); + } +} +apply_timezone(); + + session_start(); require_once __DIR__ . '/lang.php'; diff --git a/includes/pages/appointments.php b/includes/pages/appointments.php index c244a49..5fc16e3 100644 --- a/includes/pages/appointments.php +++ b/includes/pages/appointments.php @@ -1,7 +1,4 @@ - - + @@ -237,8 +234,8 @@ document.addEventListener('DOMContentLoaded', function() { right: 'dayGridMonth,timeGridWeek,timeGridDay' }, allDaySlot: true, - slotMinTime: '07:00:00', - slotMaxTime: '21:00:00', + slotMinTime: '', + slotMaxTime: '', height: 'auto', themeSystem: 'bootstrap5', businessHours: true, @@ -249,12 +246,23 @@ document.addEventListener('DOMContentLoaded', function() { if (data.businessHours) { calendar.setOption('businessHours', data.businessHours); } + console.log('Events fetched:', data.events); // Debug log successCallback(data.events); }) .catch(error => failureCallback(error)); }, editable: false, selectable: true, + selectOverlap: function(event) { + if (event.extendedProps && event.extendedProps.blocks_selection) { + return false; + } + return true; + }, + selectAllow: function(selectInfo) { + // Also optionally check if we can select + return true; + }, select: function(info) { showCreateModal(info.start); calendar.unselect(); @@ -292,11 +300,66 @@ document.addEventListener('DOMContentLoaded', function() { // Initialize Select2 after some delay to ensure modal is ready $('#appointmentDetailsModal').on('shown.bs.modal', function() { initAptSelect2(); + validateHolidayFrontend(); }); + + $('#apt_doctor_id').on('change', validateHolidayFrontend); + $('#apt_start_time').on('change', validateHolidayFrontend); + + function validateHolidayFrontend() { + var docId = $('#apt_doctor_id').val(); + var startTimeStr = $('#apt_start_time').val(); + var btnSave = document.getElementById('btnSaveApt'); + + if (!docId || !startTimeStr) return; + + var datePrefix = startTimeStr.split('T')[0]; + var events = calendar.getEvents(); + var isHoliday = false; + + for (var i = 0; i < events.length; i++) { + var ev = events[i]; + // We check against the allDay visible event OR background events + if (ev.extendedProps && ev.extendedProps.type === 'doctor_holiday' && ev.extendedProps.doctor_id == docId) { + var evStartStr = ev.startStr.split('T')[0]; + var evEndStr = ev.end ? ev.endStr.split('T')[0] : evStartStr; + + // If it's an allDay event, the end date might be exclusive (e.g. 17th means up to 16th 23:59:59) + // If it's our new daily block background event, start and end are the same day + if (ev.allDay) { + if (datePrefix >= evStartStr && datePrefix < evEndStr) { + isHoliday = true; + break; + } + } else { + if (datePrefix === evStartStr) { + isHoliday = true; + break; + } + } + } + } + + if (isHoliday) { + if ($('#holidayWarning').length === 0) { + $('