38960-vm/includes/pages/appointments.php
2026-03-16 10:31:04 +00:00

378 lines
17 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> 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'); ?> (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">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">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">Cancelled</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">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']; ?>"><?php echo htmlspecialchars($p['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small text-muted"><?php echo __('doctor'); ?></label>
<select id="apt_doctor_id" class="form-select select2-modal-apt">
<?php foreach ($all_doctors as $d): ?>
<option value="<?php echo $d['id']; ?>"><?php echo htmlspecialchars($d['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">Scheduled</option>
<option value="Completed">Completed</option>
<option value="Cancelled">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 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';
document.getElementById('btnDeleteApt').style.display = 'none';
if (startTime) {
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);
}
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 data = {
action: id ? 'update' : 'create',
id: id,
patient_id: document.getElementById('apt_patient_id').value,
doctor_id: document.getElementById('apt_doctor_id').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 => 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'));
}
});
}
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 => 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'));
}
});
}
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',
headerToolbar: {
left: 'prev,next today',
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 => response.json())
.then(data => {
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();
},
eventClick: function(info) {
if (info.event.extendedProps.type === 'appointment') {
var props = info.event.extendedProps;
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');
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();
});
$('#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) {
$('<div id="holidayWarning" class="alert alert-warning mt-3 mb-0 small"><i class="bi bi-exclamation-triangle"></i> Selected doctor is on holiday on this date.</div>').appendTo('#appointmentDetailsModal .modal-body');
}
btnSave.disabled = true;
} else {
$('#holidayWarning').remove();
btnSave.disabled = false;
}
}
});
</script>
<style>
/* 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>