512 lines
19 KiB
HTML
512 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n static %}
|
|
|
|
{% block title %}{% trans "Appointment Calendar" %} - Tenhal{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
/* Calendar Event Styling */
|
|
.fc-event {
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
padding: 2px 4px;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
/* Event title wrapping and styling */
|
|
.fc-event-title {
|
|
white-space: normal !important;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
color: #fff !important;
|
|
line-height: 1.3;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.fc-event-time {
|
|
color: #fff !important;
|
|
white-space: normal !important;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.fc-daygrid-event {
|
|
white-space: normal !important;
|
|
}
|
|
|
|
.fc-daygrid-event-dot {
|
|
display: none;
|
|
}
|
|
|
|
/* Prevent hover color change - keep original background */
|
|
.fc-event:hover,
|
|
.fc-daygrid-event:hover {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.fc-event-booked:hover { background-color: #00acac !important; border-color: #00acac !important; }
|
|
.fc-event-confirmed:hover { background-color: #348fe2 !important; border-color: #348fe2 !important; }
|
|
.fc-event-rescheduled:hover { background-color: #f59c1a !important; border-color: #f59c1a !important; }
|
|
.fc-event-cancelled:hover { background-color: #ff5b57 !important; border-color: #ff5b57 !important; }
|
|
.fc-event-no_show:hover { background-color: #2d353c !important; border-color: #2d353c !important; }
|
|
.fc-event-arrived:hover { background-color: #00acac !important; border-color: #00acac !important; }
|
|
.fc-event-in_progress:hover { background-color: #f59c1a !important; border-color: #f59c1a !important; }
|
|
.fc-event-completed:hover { background-color: #00acac !important; border-color: #00acac !important; }
|
|
.fc-event-booked { background-color: #00acac; border-color: #00acac; }
|
|
.fc-event-confirmed { background-color: #348fe2; border-color: #348fe2; }
|
|
.fc-event-rescheduled { background-color: #f59c1a; border-color: #f59c1a; color: #000; }
|
|
.fc-event-cancelled { background-color: #ff5b57; border-color: #ff5b57; }
|
|
.fc-event-no_show { background-color: #2d353c; border-color: #2d353c; }
|
|
.fc-event-arrived { background-color: #00acac; border-color: #00acac; }
|
|
.fc-event-in_progress { background-color: #f59c1a; border-color: #f59c1a; }
|
|
.fc-event-completed { background-color: #00acac; border-color: #00acac; }
|
|
|
|
/* Event List Sidebar Styling */
|
|
.fc-event-list {
|
|
background: #fff;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
|
}
|
|
|
|
.fc-event-list .fc-event {
|
|
position: relative;
|
|
display: block;
|
|
padding: 8px 12px;
|
|
margin-bottom: 8px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: #f8f9fa;
|
|
cursor: default;
|
|
transition: all .2s ease;
|
|
}
|
|
|
|
.fc-event-list .fc-event:hover {
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.fc-event-list .fc-event-text {
|
|
display: inline-block;
|
|
font-size: 13px;
|
|
color: #2d353c;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.fc-event-list .fc-event-icon {
|
|
float: right;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.fc-event-list h5 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #2d353c;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.fc-event-list hr {
|
|
margin: 15px 0;
|
|
border-top: 1px solid #e2e7eb;
|
|
}
|
|
|
|
/* Filter Section in Sidebar */
|
|
.filter-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.filter-section .form-label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #2d353c;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
|
|
.filter-section .form-select {
|
|
font-size: 13px;
|
|
padding: 6px 10px;
|
|
height: auto;
|
|
border-color: #e2e7eb;
|
|
}
|
|
|
|
.filter-section .btn {
|
|
font-size: 13px;
|
|
padding: 6px 12px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Calendar Container */
|
|
.calendar {
|
|
{#background: #fff;#}
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
|
}
|
|
|
|
/* Page Header Styling */
|
|
.page-header {
|
|
margin-bottom: 15px;
|
|
font-size: 24px;
|
|
font-weight: 300;
|
|
color: #2d353c;
|
|
}
|
|
|
|
.page-header small {
|
|
font-size: 14px;
|
|
color: #707478;
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* Breadcrumb Styling */
|
|
.breadcrumb {
|
|
background: transparent;
|
|
padding: 0;
|
|
margin-bottom: 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.breadcrumb-item a {
|
|
color: #348fe2;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.breadcrumb-item.active {
|
|
color: #707478;
|
|
}
|
|
|
|
/* Action Buttons */
|
|
.action-buttons {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.action-buttons .btn {
|
|
font-size: 13px;
|
|
padding: 6px 12px;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 991.98px) {
|
|
.fc-event-list {
|
|
margin-bottom: 20px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- BEGIN breadcrumb -->
|
|
<ol class="breadcrumb float-xl-end">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">{% trans "Home" %}</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">{% trans "Appointments" %}</a></li>
|
|
<li class="breadcrumb-item active">{% trans "Calendar" %}</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">{% trans "Calendar" %} <small>{% trans "appointment scheduling and management" %}</small></h1>
|
|
<!-- END page-header -->
|
|
|
|
<hr />
|
|
|
|
<!-- BEGIN action buttons -->
|
|
<div class="action-buttons text-end">
|
|
{% if user.role in 'ADMIN,FRONT_DESK' %}
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#quickBookModal">
|
|
<i class="fas fa-plus me-1"></i>{% trans "Quick Book" %}
|
|
</button>
|
|
{% endif %}
|
|
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-list me-1"></i>{% trans "List View" %}
|
|
</a>
|
|
</div>
|
|
<!-- END action buttons -->
|
|
|
|
<!-- BEGIN row -->
|
|
<div class="row">
|
|
<!-- BEGIN event-list -->
|
|
<div class="d-none d-lg-block" style="width: 215px">
|
|
<div class="fc-event-list">
|
|
<h5>{% trans "Filters" %}</h5>
|
|
|
|
<!-- Clinic Filter -->
|
|
<div class="filter-section">
|
|
<label class="form-label">{% trans "Clinic" %}</label>
|
|
<select name="clinic" id="clinicFilter" class="form-select">
|
|
<option value="">{% trans "All Clinics" %}</option>
|
|
{% for clinic in clinics %}
|
|
<option value="{{ clinic.id }}">{{ clinic.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Provider Filter -->
|
|
<div class="filter-section">
|
|
<label class="form-label">{% trans "Provider" %}</label>
|
|
<select name="provider" id="providerFilter" class="form-select">
|
|
<option value="">{% trans "All Providers" %}</option>
|
|
{% for provider in providers %}
|
|
<option value="{{ provider.id }}">{{ provider.get_full_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Status Filter -->
|
|
<div class="filter-section">
|
|
<label class="form-label">{% trans "Status" %}</label>
|
|
<select name="status" id="statusFilter" class="form-select">
|
|
<option value="">{% trans "All Statuses" %}</option>
|
|
<option value="BOOKED">{% trans "Booked" %}</option>
|
|
<option value="CONFIRMED">{% trans "Confirmed" %}</option>
|
|
<option value="ARRIVED">{% trans "Arrived" %}</option>
|
|
<option value="IN_PROGRESS">{% trans "In Progress" %}</option>
|
|
<option value="COMPLETED">{% trans "Completed" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Apply Button -->
|
|
<div class="filter-section">
|
|
<button type="button" id="applyFilters" class="btn btn-primary">
|
|
<i class="fas fa-filter me-1"></i>{% trans "Apply" %}
|
|
</button>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<h5>{% trans "Status Legend" %}</h5>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "Booked" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #00acac;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "Confirmed" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #348fe2;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "Arrived" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #00acac;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "In Progress" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #f59c1a;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "Completed" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #00acac;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "Rescheduled" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #f59c1a;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "Cancelled" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #ff5b57;"></i></div>
|
|
</div>
|
|
<div class="fc-event">
|
|
<div class="fc-event-text">{% trans "No Show" %}</div>
|
|
<div class="fc-event-icon"><i class="fas fa-circle fa-fw fs-9px" style="color: #2d353c;"></i></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END event-list -->
|
|
|
|
<div class="col-lg">
|
|
<!-- BEGIN calendar -->
|
|
<div id="calendar" class="calendar"></div>
|
|
<!-- END calendar -->
|
|
</div>
|
|
</div>
|
|
<!-- END row -->
|
|
|
|
<!-- Quick Book Modal -->
|
|
<div class="modal fade" id="quickBookModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{% trans "Quick Book Appointment" %}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-muted">{% trans "Click on a date/time in the calendar to book an appointment, or use the full form for more options." %}</p>
|
|
<a href="{% url 'appointments:appointment_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-calendar-plus me-1"></i>{% trans "Go to Full Booking Form" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Appointment Detail Modal -->
|
|
<div class="modal fade" id="appointmentModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="appointmentModalTitle">{% trans "Appointment Details" %}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="appointmentModalBody">
|
|
<!-- Content loaded via AJAX -->
|
|
<div class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">{% trans "Loading..." %}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
|
<a href="#" id="viewFullDetails" class="btn btn-primary">{% trans "View Full Details" %}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'plugins/moment/min/moment.min.js' %}"></script>
|
|
<script src="{% static 'plugins/@fullcalendar/core/index.global.js' %}"></script>
|
|
<script src="{% static 'plugins/@fullcalendar/daygrid/index.global.js' %}"></script>
|
|
<script src="{% static 'plugins/@fullcalendar/timegrid/index.global.js' %}"></script>
|
|
<script src="{% static 'plugins/@fullcalendar/interaction/index.global.js' %}"></script>
|
|
<script src="{% static 'plugins/@fullcalendar/list/index.global.js' %}"></script>
|
|
<script src="{% static 'plugins/@fullcalendar/bootstrap/index.global.js' %}"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var calendarEl = document.getElementById('calendar');
|
|
var appointmentModal = new bootstrap.Modal(document.getElementById('appointmentModal'));
|
|
|
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
|
initialView: 'dayGridMonth',
|
|
headerToolbar: {
|
|
left: 'dayGridMonth,timeGridWeek,timeGridDay',
|
|
center: 'title',
|
|
right: 'prev,next today'
|
|
},
|
|
buttonText: {
|
|
today: '{% trans "Today" %}',
|
|
month: '{% trans "Month" %}',
|
|
week: '{% trans "Week" %}',
|
|
day: '{% trans "Day" %}'
|
|
},
|
|
themeSystem: 'bootstrap',
|
|
locale: '{{ LANGUAGE_CODE }}',
|
|
firstDay: 0, // Sunday
|
|
slotMinTime: '08:00:00',
|
|
slotMaxTime: '20:00:00',
|
|
slotDuration: '00:30:00',
|
|
allDaySlot: false,
|
|
nowIndicator: true,
|
|
navLinks: true,
|
|
editable: false,
|
|
selectable: true,
|
|
selectMirror: true,
|
|
dayMaxEvents: true,
|
|
weekends: true,
|
|
|
|
// Load events
|
|
events: function(info, successCallback, failureCallback) {
|
|
var params = new URLSearchParams({
|
|
start: info.startStr,
|
|
end: info.endStr,
|
|
clinic: document.getElementById('clinicFilter').value,
|
|
provider: document.getElementById('providerFilter').value,
|
|
status: document.getElementById('statusFilter').value
|
|
});
|
|
|
|
fetch('/api/v1/appointments/calendar/?' + params.toString())
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
var events = data.map(function(appointment) {
|
|
return {
|
|
id: appointment.id,
|
|
title: appointment.patient_name + ' - ' + appointment.service_type,
|
|
start: appointment.scheduled_date + 'T' + appointment.scheduled_time,
|
|
end: appointment.end_time,
|
|
className: 'fc-event-' + appointment.status.toLowerCase(),
|
|
extendedProps: {
|
|
appointmentNumber: appointment.appointment_number,
|
|
patientName: appointment.patient_name,
|
|
patientMRN: appointment.patient_mrn,
|
|
clinicName: appointment.clinic_name,
|
|
providerName: appointment.provider_name,
|
|
serviceType: appointment.service_type,
|
|
status: appointment.status,
|
|
room: appointment.room
|
|
}
|
|
};
|
|
});
|
|
successCallback(events);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading appointments:', error);
|
|
failureCallback(error);
|
|
});
|
|
},
|
|
|
|
// Event click
|
|
eventClick: function(info) {
|
|
info.jsEvent.preventDefault();
|
|
loadAppointmentDetails(info.event.id);
|
|
},
|
|
|
|
// Date select (for quick booking)
|
|
select: function(info) {
|
|
{% if user.role in 'ADMIN,FRONT_DESK' %}
|
|
if (confirm('{% trans "Book an appointment for" %} ' + info.startStr + '?')) {
|
|
var url = '{% url "appointments:appointment_create" %}' +
|
|
'?date=' + info.startStr.split('T')[0] +
|
|
'&time=' + info.startStr.split('T')[1].substring(0, 5);
|
|
window.location.href = url;
|
|
}
|
|
{% endif %}
|
|
calendar.unselect();
|
|
},
|
|
|
|
// Event content
|
|
eventContent: function(arg) {
|
|
var timeText = arg.timeText;
|
|
var title = arg.event.title;
|
|
|
|
return {
|
|
html: '<div class="fc-event-main-frame">' +
|
|
'<div class="fc-event-time">' + timeText + '</div>' +
|
|
'<div class="fc-event-title-container">' +
|
|
'<div class="fc-event-title">' + title + '</div>' +
|
|
'</div>' +
|
|
'</div>'
|
|
};
|
|
}
|
|
});
|
|
|
|
calendar.render();
|
|
|
|
// Apply filters
|
|
document.getElementById('applyFilters').addEventListener('click', function() {
|
|
calendar.refetchEvents();
|
|
});
|
|
|
|
// Load appointment details
|
|
function loadAppointmentDetails(appointmentId) {
|
|
document.getElementById('appointmentModalBody').innerHTML =
|
|
'<div class="text-center py-5">' +
|
|
'<div class="spinner-border text-primary" role="status">' +
|
|
'<span class="visually-hidden">{% trans "Loading..." %}</span>' +
|
|
'</div></div>';
|
|
|
|
document.getElementById('viewFullDetails').href =
|
|
'/appointments/' + appointmentId + '/';
|
|
|
|
fetch('/appointments/' + appointmentId + '/quick-view/')
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
document.getElementById('appointmentModalBody').innerHTML = html;
|
|
appointmentModal.show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading appointment:', error);
|
|
document.getElementById('appointmentModalBody').innerHTML =
|
|
'<div class="alert alert-danger">{% trans "Error loading appointment details" %}</div>';
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|