38960-vm/includes/pages/appointments.php
2026-03-22 03:18:19 +00:00

566 lines
25 KiB
PHP

<?php $s = get_system_settings(); ?>
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/main.min.css' rel='stylesheet' />
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="fw-bold text-secondary"><?php echo __('appointments'); ?></h3>
<div>
<button class="btn btn-outline-secondary shadow-sm me-2" onclick="printAppointments()">
<i class="bi bi-printer me-1"></i> <?php echo __('print_daily_list'); ?>
</button>
<button class="btn btn-primary shadow-sm" onclick="showCreateModal()">
<i class="bi bi-calendar-plus me-1"></i> <?php echo __('book_appointment'); ?>
</button>
</div>
</div>
<!-- Filters -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label small text-muted"><?php echo __('doctor'); ?> (<?php echo __('filter'); ?>)</label>
<select id="doctorFilter" class="form-select bg-light">
<option value=""><?php echo __('all'); ?> <?php echo __('doctors'); ?></option>
<?php foreach ($all_doctors as $d): ?>
<option value="<?php echo $d['id']; ?>"><?php echo htmlspecialchars($d['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-8">
<div class="d-flex align-items-end h-100 pb-1 flex-wrap">
<div class="d-flex align-items-center me-3 mb-2">
<div class="badge bg-primary me-2" style="width: 15px; height: 15px; border-radius: 50%;">&nbsp;</div>
<small class="text-muted"><?php echo __('scheduled'); ?></small>
</div>
<div class="d-flex align-items-center me-3 mb-2">
<div class="badge bg-success me-2" style="width: 15px; height: 15px; border-radius: 50%;">&nbsp;</div>
<small class="text-muted"><?php echo __('completed'); ?></small>
</div>
<div class="d-flex align-items-center me-3 mb-2">
<div class="badge bg-danger me-2" style="width: 15px; height: 15px; border-radius: 50%;">&nbsp;</div>
<small class="text-muted"><?php echo __('cancelled'); ?></small>
</div>
<div class="d-flex align-items-center me-3 mb-2">
<div class="badge me-2" style="width: 15px; height: 15px; border-radius: 50%; background-color: #fd7e14;">&nbsp;</div>
<small class="text-muted"><?php echo __('home_visits'); ?></small>
</div>
<div class="d-flex align-items-center mb-2">
<div class="badge bg-warning me-2" style="width: 15px; height: 15px; border-radius: 50%;">&nbsp;</div>
<small class="text-muted">Holiday</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body p-4">
<div id='calendar'></div>
</div>
</div>
<!-- Appointment Details/Edit Modal -->
<div class="modal fade" id="appointmentDetailsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold text-secondary" id="modalTitle"><?php echo __('appointment_details'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-3">
<input type="hidden" id="apt_id">
<div class="mb-3">
<label class="form-label small text-muted"><?php echo __('patient'); ?></label>
<select id="apt_patient_id" class="form-select select2-modal-apt">
<?php foreach ($all_patients as $p): ?>
<option value="<?php echo $p['id']; ?>" data-address="<?php echo htmlspecialchars($p['address'] ?? ''); ?>"><?php echo htmlspecialchars($p['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label small text-muted">Visit Type</label>
<select id="apt_visit_type" class="form-select" onchange="toggleAddressField()">
<option value="Clinic">Clinic Visit</option>
<option value="Home">Home Visit</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small text-muted"><?php echo __('provider'); ?></label>
<select id="apt_provider_type" class="form-select" onchange="toggleProviderField()">
<option value="Doctor"><?php echo __('doctor'); ?></option>
<option value="Nurse"><?php echo __('nurse'); ?></option>
</select>
</div>
</div>
<div class="mb-3 d-none" id="div_address">
<label class="form-label small text-muted">Address (For Home Visit)</label>
<textarea id="apt_address" class="form-control" rows="2" placeholder="Enter patient address..."></textarea>
</div>
<div class="mb-3" id="div_doctor">
<label class="form-label small text-muted"><?php echo __('doctor'); ?></label>
<select id="apt_doctor_id" class="form-select select2-modal-apt">
<option value=""><?php echo __('select_doctor'); ?></option>
<?php foreach ($all_doctors as $d): ?>
<option value="<?php echo $d['id']; ?>"><?php echo htmlspecialchars($d['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3 d-none" id="div_nurse">
<label class="form-label small text-muted"><?php echo __('nurse'); ?></label>
<select id="apt_nurse_id" class="form-select select2-modal-apt">
<option value="">Select Nurse</option>
<?php foreach ($all_nurses as $n): ?>
<option value="<?php echo $n['id']; ?>"><?php echo htmlspecialchars($n['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small text-muted"><?php echo __('date'); ?> & <?php echo __('time'); ?></label>
<input type="datetime-local" id="apt_start_time" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small text-muted"><?php echo __('status'); ?></label>
<select id="apt_status" class="form-select">
<option value="Scheduled"><?php echo __('scheduled'); ?></option>
<option value="Completed"><?php echo __('completed'); ?></option>
<option value="Cancelled"><?php echo __('cancelled'); ?></option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label small text-muted"><?php echo __('reason'); ?></label>
<textarea id="apt_reason" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer border-0 bg-light rounded-bottom">
<div class="d-flex justify-content-between w-100">
<button type="button" class="btn btn-outline-danger shadow-sm" id="btnDeleteApt" onclick="deleteAppointment()">
<i class="bi bi-trash"></i> <?php echo __('delete'); ?>
</button>
<div>
<button type="button" class="btn btn-secondary shadow-sm me-2" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="button" class="btn btn-primary shadow-sm px-4" id="btnSaveApt" onclick="saveAppointment()">
<i class="bi bi-check2-circle me-1"></i> <?php echo __('save'); ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var calendar;
function initAptSelect2() {
if (typeof jQuery !== 'undefined' && jQuery.fn.select2) {
$('.select2-modal-apt').each(function() {
$(this).select2({
dropdownParent: $('#appointmentDetailsModal'),
theme: 'bootstrap-5',
width: '100%'
});
});
}
}
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 validateHolidayFrontend() {
var btnSave = document.getElementById('btnSaveApt');
$('#holidayWarning').remove();
if (btnSave) btnSave.disabled = false;
var providerType = $('#apt_provider_type').val();
if (providerType === 'Nurse') return;
var docId = $('#apt_doctor_id').val();
var startTimeStr = $('#apt_start_time').val();
if (!docId || !startTimeStr) return;
// Use API to check for holiday definitively (independent of calendar view)
if (btnSave) btnSave.disabled = true; // Disable while checking
fetch('api/doctor_holidays.php?doctor_id=' + docId)
.then(response => response.json())
.then(data => {
// Re-check current values to ensure they haven't changed while fetching
var currentDocId = $('#apt_doctor_id').val();
var currentStartTimeStr = $('#apt_start_time').val();
if (currentDocId != docId || currentStartTimeStr != startTimeStr) return;
// Remove any existing warning to prevent duplication (especially from rapid successive calls)
$('#holidayWarning').remove();
if (data.success && data.holidays) {
var isHoliday = false;
var datePrefix = startTimeStr.split('T')[0];
// Check if date is in any holiday range
for (var i = 0; i < data.holidays.length; i++) {
var h = data.holidays[i];
// h.start_date and h.end_date are YYYY-MM-DD
if (datePrefix >= h.start_date && datePrefix <= h.end_date) {
isHoliday = true;
break;
}
}
if (isHoliday) {
$('<div id="holidayWarning" class="alert alert-warning mt-3 mb-0 small"><i class="bi bi-exclamation-triangle"></i> <?php echo __('doctor_is_on_holiday_on_this_date'); ?></div>').appendTo('#appointmentDetailsModal .modal-body');
if (btnSave) btnSave.disabled = true;
} else {
if (btnSave) btnSave.disabled = false;
}
} else {
if (btnSave) btnSave.disabled = false;
}
})
.catch(err => {
console.error('Error checking holiday:', err);
if (btnSave) btnSave.disabled = false;
});
}
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');
} 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 = '<?php echo __('book_appointment'); ?>';
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 && 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 {
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) {
$('#apt_doctor_id').val(document.getElementById('doctorFilter').value).trigger('change');
}
// Initialize/reset modal
var modalEl = document.getElementById('appointmentDetailsModal');
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
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: 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
};
fetch('api/appointments.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.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');
var modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
calendar.refetchEvents();
} 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.');
});
}
function deleteAppointment() {
var id = document.getElementById('apt_id').value;
if (!id || !confirm('Are you sure you want to delete this appointment?')) return;
fetch('api/appointments.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', id: id })
})
.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');
var modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
calendar.refetchEvents();
} 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.');
});
}
function printAppointments() {
var doctorId = document.getElementById('doctorFilter').value;
// Get the date that is currently focused in the calendar
var date = calendar.getDate();
// Format as YYYY-MM-DD using local time to avoid timezone shifts
var offset = date.getTimezoneOffset() * 60000;
var dateStr = (new Date(date.getTime() - offset)).toISOString().slice(0, 10);
window.open('print_appointments.php?date=' + dateStr + '&doctor_id=' + doctorId, '_blank');
}
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var doctorFilter = document.getElementById('doctorFilter');
calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'timeGridWeek',
customButtons: {
customToday: {
text: '<?php echo __('today'); ?>',
click: function() {
calendar.today();
}
}
},
headerToolbar: {
left: 'prev,next customToday',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
allDaySlot: true,
slotMinTime: '<?php echo substr(($s['working_hours_start'] ?? '07:00'), 0, 5) . ':00'; ?>',
slotMaxTime: '<?php echo substr(($s['working_hours_end'] ?? '21:00'), 0, 5) . ':00'; ?>',
height: 'auto',
themeSystem: 'bootstrap5',
businessHours: true,
events: function(fetchInfo, successCallback, failureCallback) {
fetch('api/appointments.php?start=' + fetchInfo.startStr + '&end=' + fetchInfo.endStr + '&doctor_id=' + doctorFilter.value)
.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);
})
.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();
},
eventClick: function(info) {
if (info.event.extendedProps.type === 'appointment') {
var props = info.event.extendedProps;
document.getElementById('modalTitle').innerText = '<?php echo __('edit_appointment'); ?>';
document.getElementById('apt_id').value = info.event.id;
$('#apt_patient_id').val(props.patient_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;
var localISOTime = (new Date(start.getTime() - offset)).toISOString().slice(0, 16);
document.getElementById('apt_start_time').value = localISOTime;
document.getElementById('apt_status').value = props.status;
document.getElementById('apt_reason').value = props.reason || '';
document.getElementById('btnDeleteApt').style.display = 'block';
var modalEl = document.getElementById('appointmentDetailsModal');
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
}
});
calendar.render();
doctorFilter.addEventListener('change', function() {
calendar.refetchEvents();
});
// Initialize Select2 after some delay to ensure modal is ready
$('#appointmentDetailsModal').on('shown.bs.modal', 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
});
</script>
<style>
.public-holiday-event {
background: repeating-linear-gradient(
45deg,
#dc3545,
#dc3545 10px,
#e4606d 10px,
#e4606d 20px
) !important;
border: 1px solid #c82333 !important;
opacity: 0.8;
pointer-events: none;
}
.doctor-holiday-event {
background: repeating-linear-gradient(
45deg,
#ffc107,
#ffc107 10px,
#ffca2c 10px,
#ffca2c 20px
) !important;
border: 1px solid #e0a800 !important;
color: #856404 !important;
opacity: 0.8;
pointer-events: none; /* Make it unclickable so users can click behind it if needed */
}
/* Standard event styles now */
.doctor-holiday-bg {
border: 1px solid #e0a800 !important;
font-weight: bold;
}
.fc-event { cursor: pointer; border: none; padding: 2px; transition: transform 0.1s ease; }
.fc-event:hover { transform: scale(1.02); z-index: 5; }
.fc-event-title { font-weight: 500; font-size: 0.85rem; }
.fc-col-header-cell { background-color: #f8f9fa; padding: 10px 0 !important; border-bottom: 2px solid #dee2e6 !important; }
.fc-day-today { background-color: rgba(0, 45, 98, 0.05) !important; }
.fc-toolbar-title { font-weight: 700; color: #002D62; font-size: 1.5rem; }
.fc .fc-button-primary { background-color: #002D62; border-color: #002D62; text-transform: capitalize; }
.fc .fc-button-primary:hover { background-color: #003a80; border-color: #003a80; }
.fc .fc-button-primary:disabled { background-color: #002D62; border-color: #002D62; opacity: 0.65; }
.fc-nonbusiness { background-color: rgba(108, 117, 125, 0.1) !important; }
.fc-v-event { box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-radius: 4px; }
.select2-container--bootstrap-5 .select2-selection { border-color: #dee2e6; background-color: #f8f9fa; }
.modal-header { border-bottom: none; }
.modal-footer { border-top: none; }
</style>