agdar/appointments/templates/appointments/appointment_calendar.html
Marwan Alwali 3fbfccb799 update
2025-11-06 23:08:02 +03:00

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 %}