Marwan Alwali 263292f6be update
2025-11-04 00:50:06 +03:00

4210 lines
147 KiB
Python

"""
Appointments app views for hospital management system with comprehensive CRUD operations.
"""
from django.contrib.messages import success
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.template.defaulttags import csrf_token
from django.utils.dateparse import parse_datetime
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import (
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
)
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib import messages
from django.db.models.functions import Now
from django.db.models import Q, Count, Avg, Case, When, Value, DurationField, FloatField, F, ExpressionWrapper, IntegerField
from django.utils import timezone
from django.urls import reverse_lazy, reverse
from django.core.paginator import Paginator
from datetime import timedelta, datetime, time, date
from django.utils.translation import gettext_lazy as _
from hr.models import Schedule, Employee
from .models import *
from .forms import *
from .utils import (
get_tenant_from_request,
check_appointment_conflicts,
send_appointment_notification,
generate_meeting_url,
calculate_appointment_duration,
validate_appointment_time,
get_available_time_slots,
format_appointment_summary
)
from patients.models import PatientProfile
from accounts.models import User
from core.utils import AuditLogger
class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
"""
Appointment dashboard view.
"""
template_name = 'appointments/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
if tenant:
today = timezone.now().date()
now = timezone.now()
# Today's appointments
context['todays_appointments'] = AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today
).order_by('scheduled_datetime')[:15]
# Active queues
context['active_queues'] = WaitingQueue.objects.filter(
tenant=tenant,is_active=True
).annotate(entry_count=Count('queue_entries')).order_by('-entry_count')
# Statistics
context['stats'] = {
'total_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
).count(),
'total_appointments_today': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today
).count(),
'pending_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='PENDING'
).count(),
'active_queues_count': WaitingQueue.objects.filter(
tenant=tenant,
is_active=True
).count(),
'telemedicine_sessions': TelemedicineSession.objects.filter(
appointment__tenant=tenant,
appointment__scheduled_datetime__date=today
).count(),
}
return context
class AppointmentRequestListView(LoginRequiredMixin, ListView):
"""
List appointment requests.
"""
model = AppointmentRequest
template_name = 'appointments/requests/appointment_list.html'
context_object_name = 'appointments'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
queryset = AppointmentRequest.objects.filter(tenant=tenant)
# Apply filters
appointment_type = self.request.GET.get('appointment_type')
if appointment_type:
queryset = queryset.filter(appointment_type=appointment_type)
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
priority = self.request.GET.get('priority')
if priority:
queryset = queryset.filter(priority=priority)
provider_id = self.request.GET.get('provider')
if provider_id:
queryset = queryset.filter(provider_id=provider_id)
department = self.request.GET.get('department')
if department:
queryset = queryset.filter(department__icontains=department)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(scheduled_datetime__date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(scheduled_datetime__date__lte=date_to)
is_telemedicine = self.request.GET.get('is_telemedicine')
if is_telemedicine == 'true':
queryset = queryset.filter(is_telemedicine=True)
elif is_telemedicine == 'false':
queryset = queryset.filter(is_telemedicine=False)
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(patient__first_name__icontains=search) |
Q(patient__last_name__icontains=search) |
Q(patient__patient_id__icontains=search) |
Q(provider__first_name__icontains=search) |
Q(provider__last_name__icontains=search) |
Q(reason__icontains=search) |
Q(department__icontains=search)
)
return queryset.order_by('-scheduled_datetime')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = AppointmentSearchForm(
self.request.GET,
user=self.request.user
)
return context
class AppointmentRequestDetailView(LoginRequiredMixin, DetailView):
"""
Display appointment request details.
"""
model = AppointmentRequest
template_name = 'appointments/requests/appointment_detail.html'
context_object_name = 'appointment'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
return AppointmentRequest.objects.filter(tenant=tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
appointment = self.get_object()
# Get related data
context['queue_entry'] = QueueEntry.objects.filter(
appointment=appointment
).first()
context['telemedicine_session'] = TelemedicineSession.objects.filter(
appointment=appointment
).first()
return context
class AppointmentRequestCreateView(LoginRequiredMixin, CreateView):
"""
Create new appointment request.
"""
model = AppointmentRequest
form_class = AppointmentRequestForm
template_name = 'appointments/requests/appointment_form.html'
permission_required = 'appointments.add_appointmentrequest'
success_url = reverse_lazy('appointments:appointment_request_list')
def dispatch(self, request, *args, **kwargs):
# Ensure tenant exists (if you follow multi-tenant pattern)
self.tenant = self.request.user.tenant
if not self.tenant:
return JsonResponse({"error": "No tenant found"}, status=400)
# Fetch the patient from URL and ensure it belongs to the tenant
self.patient = get_object_or_404(
PatientProfile,
pk=self.kwargs.get("pk"),
tenant=self.tenant,
is_active=True,
)
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs["initial"]["patient"] = self.patient
return kwargs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["patient"] = self.patient
return ctx
def form_valid(self, form):
# Set tenant
form.instance.tenant = self.tenant
form.instance.patient = self.patient
form.instance.status = 'PENDING'
response = super().form_valid(form)
# Log appointment creation
AuditLogger.log_event(
tenant=form.instance.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Appointment',
description=f'Created appointment: {self.object.patient} with {self.object.provider}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Appointment for {self.object.patient} created successfully.')
return response
class AppointmentRequestUpdateView(LoginRequiredMixin, UpdateView):
"""
Update appointment request (limited fields after scheduling).
"""
model = AppointmentRequest
form_class = AppointmentRequestForm
template_name = 'appointments/requests/appointment_form.html'
permission_required = 'appointments.change_appointmentrequest'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
return AppointmentRequest.objects.filter(tenant=tenant)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
return reverse('appointments:appointment_request_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log appointment update
AuditLogger.log_event(
tenant=self.object.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Appointment',
description=f'Updated appointment: {self.object.patient} with {self.object.provider}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Appointment for {self.object.patient} updated successfully.')
return response
class AppointmentRequestDeleteView(LoginRequiredMixin, DeleteView):
"""
Cancel appointment request.
"""
model = AppointmentRequest
template_name = 'appointments/requests/appointment_confirm_delete.html'
permission_required = 'appointments.delete_appointmentrequest'
success_url = reverse_lazy('appointments:appointment_request_list')
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
return AppointmentRequest.objects.filter(tenant=tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# Update status to cancelled instead of hard delete
self.object.status = 'CANCELLED'
self.object.save()
# Log appointment cancellation
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Appointment',
description=f'Cancelled appointment: {self.object.patient} with {self.object.provider}',
user=request.user,
content_object=self.object,
request=request
)
messages.success(request, f'Appointment for {self.object.patient} cancelled successfully.')
return redirect(self.success_url)
class SlotAvailabilityListView(LoginRequiredMixin, ListView):
"""
List slot availability.
"""
model = SlotAvailability
template_name = 'appointments/slots/slot_list.html'
context_object_name = 'slots'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
queryset = SlotAvailability.objects.filter(provider__tenant=tenant)
# Apply filters
provider_id = self.request.GET.get('provider')
if provider_id:
queryset = queryset.filter(provider_id=provider_id)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(date__lte=date_to)
availability = self.request.GET.get('availability')
if availability == 'available':
queryset = queryset.filter(is_available=True)
elif availability == 'unavailable':
queryset = queryset.filter(is_available=False)
return queryset.order_by('date', 'start_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = SlotSearchForm(
self.request.GET,
user=self.request.user
)
return context
class SlotAvailabilityDetailView(LoginRequiredMixin, DetailView):
"""
Display slot availability details.
"""
model = SlotAvailability
template_name = 'appointments/slots/slot_detail.html'
context_object_name = 'slot'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
return SlotAvailability.objects.filter(provider__tenant=tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
slot = self.get_object()
# Get appointments for this slot
context['appointments'] = AppointmentRequest.objects.filter(
provider=slot.provider,
scheduled_datetime__date=slot.date,
scheduled_datetime__time__gte=slot.start_time,
scheduled_datetime__time__lt=slot.end_time
).order_by('scheduled_datetime')
return context
class SlotAvailabilityCreateView(LoginRequiredMixin, CreateView):
"""
Create new slot availability.
"""
model = SlotAvailability
form_class = SlotAvailabilityForm
template_name = 'appointments/slots/slot_form.html'
permission_required = 'appointments.add_slotavailability'
success_url = reverse_lazy('appointments:slot_availability_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
response = super().form_valid(form)
# Log slot creation
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Slot Availability',
description=f'Created slot: {self.object.provider} on {self.object.date}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Slot availability for {self.object.provider} created successfully.')
return response
class SlotAvailabilityUpdateView(LoginRequiredMixin, UpdateView):
"""
Update slot availability.
"""
model = SlotAvailability
form_class = SlotAvailabilityForm
template_name = 'appointments/slots/slot_form.html'
permission_required = 'appointments.change_slotavailability'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
return SlotAvailability.objects.filter(provider__tenant=tenant)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
return reverse('appointments:slot_availability_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log slot update
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Slot Availability',
description=f'Updated slot: {self.object.provider} on {self.object.date}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Slot availability for {self.object.provider} updated successfully.')
return response
class SlotAvailabilityDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete slot availability.
"""
model = SlotAvailability
template_name = 'appointments/slots/slot_confirm_delete.html'
permission_required = 'appointments.delete_slotavailability'
success_url = reverse_lazy('appointments:slot_availability_list')
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
return SlotAvailability.objects.filter(provider__tenant=tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# Check if slot has appointments
appointments_count = AppointmentRequest.objects.filter(
provider=self.object.provider,
scheduled_datetime__date=self.object.date,
scheduled_datetime__time__gte=self.object.start_time,
scheduled_datetime__time__lt=self.object.end_time,
status__in=['SCHEDULED', 'CONFIRMED']
).count()
if appointments_count > 0:
messages.error(request, f'Cannot delete slot with {appointments_count} scheduled appointments.')
return redirect('appointments:slot_availability_detail', pk=self.object.pk)
provider_name = str(self.object.provider)
slot_date = self.object.date
# Log slot deletion
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='DELETE',
event_category='APPOINTMENT_MANAGEMENT',
action='Delete Slot Availability',
description=f'Deleted slot: {provider_name} on {slot_date}',
user=request.user,
content_object=self.object,
request=request
)
messages.success(request, f'Slot availability for {provider_name} on {slot_date} deleted successfully.')
return super().delete(request, *args, **kwargs)
class WaitingQueueListView(LoginRequiredMixin, ListView):
"""
List waiting queues.
"""
model = WaitingQueue
template_name = 'appointments/queue/waiting_queue_list.html'
context_object_name = 'queues'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
queryset = WaitingQueue.objects.filter(tenant=tenant)
# Apply filters
queue_type = self.request.GET.get('queue_type')
if queue_type:
queryset = queryset.filter(queue_type=queue_type)
status = self.request.GET.get('status')
if status == 'active':
queryset = queryset.filter(is_active=True)
elif status == 'inactive':
queryset = queryset.filter(is_active=False)
department = self.request.GET.get('department')
if department:
queryset = queryset.filter(department__icontains=department)
provider_id = self.request.GET.get('provider')
if provider_id:
queryset = queryset.filter(provider_id=provider_id)
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(name__icontains=search) |
Q(description__icontains=search) |
Q(department__icontains=search)
)
return queryset.order_by('name')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = QueueSearchForm
return context
class WaitingQueueDetailView(LoginRequiredMixin, DetailView):
"""
Display waiting queue details.
"""
model = WaitingQueue
template_name = 'appointments/queue/waiting_queue_detail.html'
context_object_name = 'queue'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
return WaitingQueue.objects.filter(tenant=tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
queue = self.get_object()
# Get queue entries
context['queue_entries'] = QueueEntry.objects.filter(
queue=queue
).order_by('queue_position', 'joined_at')
# Calculate statistics
context['stats'] = {
'total_entries': QueueEntry.objects.filter(queue=queue).count(),
'waiting_entries': QueueEntry.objects.filter(queue=queue, status='WAITING').count(),
'in_progress_entries': QueueEntry.objects.filter(queue=queue, status='IN_PROGRESS').count(),
'served_today': QueueEntry.objects.filter(queue=queue, status='COMPLETED').count(),
'no_show_entries': QueueEntry.objects.filter(queue=queue, status='NO_SHOW').count(),
# 'average_wait_time': QueueEntry.objects.filter(
# queue=queue,
# status='COMPLETED'
# ).aggregate(avg_wait=Avg('queue_entries__wait_time_minutes'))['avg_wait'] or 0,
}
return context
class WaitingQueueCreateView(LoginRequiredMixin, CreateView):
"""
Create new waiting queue.
"""
model = WaitingQueue
form_class = WaitingQueueForm
template_name = 'appointments/queue/waiting_queue_form.html'
permission_required = 'appointments.add_waitingqueue'
success_url = reverse_lazy('appointments:waiting_queue_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs['tenant'] = self.request.user.tenant
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add days of week for operating hours
context['days_of_week'] = [
{'value': 0, 'name': 'Sunday', 'enabled': False},
{'value': 1, 'name': 'Monday', 'enabled': False},
{'value': 2, 'name': 'Tuesday', 'enabled': False},
{'value': 3, 'name': 'Wednesday', 'enabled': False},
{'value': 4, 'name': 'Thursday', 'enabled': False},
{'value': 5, 'name': 'Friday', 'enabled': False},
{'value': 6, 'name': 'Saturday', 'enabled': False},
]
# Add default priority weights
context['priority_weights'] = {
'emergency': 10,
'urgent': 5,
'elderly': 2,
'pregnant': 3,
'pediatric': 2,
'regular': 1,
}
return context
def form_valid(self, form):
# Set tenant
form.instance.tenant = self.request.user.tenant
# Process operating hours from POST data
operating_hours = {}
for day in range(7):
enabled = self.request.POST.get(f'operating_hours_{day}_enabled')
if enabled:
start_time = self.request.POST.get(f'operating_hours_{day}_start')
end_time = self.request.POST.get(f'operating_hours_{day}_end')
if start_time and end_time:
operating_hours[str(day)] = {
'enabled': True,
'start_time': start_time,
'end_time': end_time
}
form.instance.operating_hours = operating_hours
# Process priority weights from POST data
priority_weights = {
'emergency': float(self.request.POST.get('priority_weight_emergency', 10)),
'urgent': float(self.request.POST.get('priority_weight_urgent', 5)),
'elderly': float(self.request.POST.get('priority_weight_elderly', 2)),
'pregnant': float(self.request.POST.get('priority_weight_pregnant', 3)),
'pediatric': float(self.request.POST.get('priority_weight_pediatric', 2)),
'regular': float(self.request.POST.get('priority_weight_regular', 1)),
}
form.instance.priority_weights = priority_weights
response = super().form_valid(form)
# Log queue creation
AuditLogger.log_event(
tenant=form.instance.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Waiting Queue',
description=f'Created queue: {self.object.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Waiting queue "{self.object.name}" created successfully.')
return response
class WaitingQueueUpdateView(LoginRequiredMixin, UpdateView):
"""
Update waiting queue.
"""
model = WaitingQueue
form_class = WaitingQueueForm
template_name = 'appointments/queue/waiting_queue_form.html'
permission_required = 'appointments.change_waitingqueue'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
return WaitingQueue.objects.filter(tenant=tenant)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs['tenant'] = self.request.user.tenant
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
queue = self.object
# Add days of week with existing operating hours
days_of_week = []
for day in range(7):
day_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
day_data = {
'value': day,
'name': day_names[day],
'enabled': False,
'start_time': '09:00',
'end_time': '17:00'
}
# Load existing operating hours if available
if queue.operating_hours and str(day) in queue.operating_hours:
hours = queue.operating_hours[str(day)]
day_data['enabled'] = hours.get('enabled', False)
day_data['start_time'] = hours.get('start_time', '09:00')
day_data['end_time'] = hours.get('end_time', '17:00')
days_of_week.append(day_data)
context['days_of_week'] = days_of_week
# Add existing priority weights or defaults
context['priority_weights'] = queue.priority_weights if queue.priority_weights else {
'emergency': 10,
'urgent': 5,
'elderly': 2,
'pregnant': 3,
'pediatric': 2,
'regular': 1,
}
return context
def get_success_url(self):
return reverse('appointments:waiting_queue_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
# Process operating hours from POST data
operating_hours = {}
for day in range(7):
enabled = self.request.POST.get(f'operating_hours_{day}_enabled')
if enabled:
start_time = self.request.POST.get(f'operating_hours_{day}_start')
end_time = self.request.POST.get(f'operating_hours_{day}_end')
if start_time and end_time:
operating_hours[str(day)] = {
'enabled': True,
'start_time': start_time,
'end_time': end_time
}
form.instance.operating_hours = operating_hours
# Process priority weights from POST data
priority_weights = {
'emergency': float(self.request.POST.get('priority_weight_emergency', 10)),
'urgent': float(self.request.POST.get('priority_weight_urgent', 5)),
'elderly': float(self.request.POST.get('priority_weight_elderly', 2)),
'pregnant': float(self.request.POST.get('priority_weight_pregnant', 3)),
'pediatric': float(self.request.POST.get('priority_weight_pediatric', 2)),
'regular': float(self.request.POST.get('priority_weight_regular', 1)),
}
form.instance.priority_weights = priority_weights
response = super().form_valid(form)
# Log queue update
AuditLogger.log_event(
tenant=self.object.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Waiting Queue',
description=f'Updated queue: {self.object.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Waiting queue "{self.object.name}" updated successfully.')
return response
class WaitingQueueDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete waiting queue.
"""
model = WaitingQueue
template_name = 'appointments/queue/waiting_queue_confirm_delete.html'
permission_required = 'appointments.delete_waitingqueue'
success_url = reverse_lazy('appointments:waiting_queue_list')
context_object_name = 'queue'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
return WaitingQueue.objects.filter(tenant=tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# Check if queue has active entries
active_entries = QueueEntry.objects.filter(
queue=self.object,
status__in=['WAITING', 'IN_PROGRESS']
).count()
if active_entries > 0:
messages.error(request, f'Cannot delete queue with {active_entries} active entries.')
return redirect('appointments:waiting_queue_detail', pk=self.object.pk)
queue_name = self.object.name
# Log queue deletion
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='DELETE',
event_category='APPOINTMENT_MANAGEMENT',
action='Delete Waiting Queue',
description=f'Deleted queue: {queue_name}',
user=request.user,
content_object=self.object,
request=request
)
messages.success(request, f'Waiting queue "{queue_name}" deleted successfully.')
return super().delete(request, *args, **kwargs)
class QueueEntryListView(LoginRequiredMixin, ListView):
"""
List queue entries.
"""
model = QueueEntry
template_name = 'appointments/queue/queue_entry_list.html'
context_object_name = 'entries'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return QueueEntry.objects.none()
queryset = QueueEntry.objects.filter(queue__tenant=tenant)
# Apply filters
queue_id = self.request.GET.get('queue_id')
if queue_id:
queryset = queryset.filter(queue_id=queue_id)
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
priority = self.request.GET.get('priority')
if priority:
queryset = queryset.filter(priority=priority)
return queryset.order_by('queue__name', 'queue_position', '-joined_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
if tenant:
context['queues'] = WaitingQueue.objects.filter(
tenant=tenant,
is_active=True
).order_by('name')
context['providers'] = User.objects.filter(
tenant=tenant,
role__in = ['PHYSICIAN', 'NURSE','NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'PHARMACIST', 'PHARMACY_TECH', 'LAB_TECH', 'RADIOLOGIST','RAD_TECH', 'THERAPIST',]
)
return context
class QueueEntryDetailView(LoginRequiredMixin, DetailView):
"""
Display queue entry details.
"""
model = QueueEntry
template_name = 'appointments/queue/queue_entry_detail.html'
context_object_name = 'entry'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return QueueEntry.objects.none()
return QueueEntry.objects.filter(queue__tenant=tenant)
class QueueEntryCreateView(LoginRequiredMixin, CreateView):
"""
Create new queue entry.
"""
model = QueueEntry
form_class = QueueEntryForm
template_name = 'appointments/queue/queue_entry_form.html'
permission_required = 'appointments.add_queueentry'
success_url = reverse_lazy('appointments:queue_entry_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
# Set position
last_position = QueueEntry.objects.filter(
queue=form.instance.queue
).aggregate(max_position=Count('id'))['max_position'] or 0
form.instance.position = last_position + 1
response = super().form_valid(form)
# Log queue entry creation
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Queue Entry',
description=f'Added {self.object.patient} to queue: {self.object.queue.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Patient {self.object.patient} added to queue successfully.')
return response
class QueueEntryUpdateView(LoginRequiredMixin, UpdateView):
"""
Update queue entry.
"""
model = QueueEntry
form_class = QueueEntryForm
template_name = 'appointments/queue/queue_entry_form.html'
permission_required = 'appointments.change_queueentry'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return QueueEntry.objects.none()
return QueueEntry.objects.filter(queue__tenant=tenant)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
return reverse('appointments:queue_entry_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log queue entry update
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Queue Entry',
description=f'Updated queue entry for {self.object.patient}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Queue entry for {self.object.patient} updated successfully.')
return response
class QueueEntryDeleteView(LoginRequiredMixin, DeleteView):
model = QueueEntry
context_object_name = 'entry'
template_name = 'appointments/queue/queue_entry_confirm_delete.html'
permission_required = 'appointments.delete_queueentry'
success_url = reverse_lazy('appointments:queue_entry_list')
class TelemedicineSessionListView(LoginRequiredMixin, ListView):
"""
List telemedicine sessions.
"""
model = TelemedicineSession
template_name = 'appointments/telemedicine/telemedicine.html'
context_object_name = 'sessions'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return TelemedicineSession.objects.none()
queryset = TelemedicineSession.objects.filter(appointment__tenant=tenant)
# Apply filters
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
platform = self.request.GET.get('platform')
if platform:
queryset = queryset.filter(platform=platform)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(start_time__date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(start_time__date__lte=date_to)
return queryset.order_by('-start_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
if tenant:
context.update({
'status_choices': TelemedicineSession.STATUS_CHOICES,
'platform_choices': TelemedicineSession.PLATFORM_CHOICES,
})
return context
class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
"""
Display telemedicine session details.
"""
model = TelemedicineSession
template_name = 'appointments/telemedicine/telemedicine_session_detail.html'
context_object_name = 'session'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return TelemedicineSession.objects.none()
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
class TelemedicineSessionCreateView(LoginRequiredMixin, CreateView):
"""
Create new telemedicine session.
"""
model = TelemedicineSession
form_class = TelemedicineSessionForm
template_name = 'appointments/telemedicine/telemedicine_session_form.html'
permission_required = 'appointments.add_telemedicinesession'
success_url = reverse_lazy('appointments:telemedicine_session_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
response = super().form_valid(form)
# Log session creation
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Telemedicine Session',
description=f'Created telemedicine session for appointment: {self.object.appointment}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Telemedicine session created successfully.')
return response
class TelemedicineSessionUpdateView(LoginRequiredMixin, UpdateView):
"""
Update telemedicine session.
"""
model = TelemedicineSession
form_class = TelemedicineSessionForm
template_name = 'appointments/telemedicine/telemedicine_session_form.html'
permission_required = 'appointments.change_telemedicinesession'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return TelemedicineSession.objects.none()
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
return reverse('appointments:telemedicine_session_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log session update
AuditLogger.log_event(
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Telemedicine Session',
description=f'Updated telemedicine session: {self.object.appointment}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Telemedicine session updated successfully.')
return response
class AppointmentTemplateListView(LoginRequiredMixin, ListView):
"""
List appointment templates.
"""
model = AppointmentTemplate
template_name = 'appointments/templates/appointment_template_list.html'
context_object_name = 'templates'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
queryset = AppointmentTemplate.objects.filter(tenant=tenant)
# Apply filters
appointment_type = self.request.GET.get('appointment_type')
if appointment_type:
queryset = queryset.filter(appointment_type=appointment_type)
department = self.request.GET.get('department')
if department:
queryset = queryset.filter(department__icontains=department)
provider_id = self.request.GET.get('provider')
if provider_id:
queryset = queryset.filter(provider_id=provider_id)
status = self.request.GET.get('status')
if status == 'active':
queryset = queryset.filter(is_active=True)
elif status == 'inactive':
queryset = queryset.filter(is_active=False)
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(name__icontains=search) |
Q(description__icontains=search) |
Q(department__icontains=search)
)
return queryset.order_by('appointment_type', 'name')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
if tenant:
context.update({
'appointment_types': AppointmentTemplate.objects.filter(
tenant=tenant
).values_list('appointment_type', flat=True).distinct(),
'providers': User.objects.filter(
tenant=tenant,
is_active=True,
employee_profile__role__in=['DOCTOR', 'NURSE', 'SPECIALIST']
).order_by('last_name', 'first_name'),
})
return context
class AppointmentTemplateDetailView(LoginRequiredMixin, DetailView):
"""
Display appointment template details.
"""
model = AppointmentTemplate
template_name = 'appointments/templates/appointment_template_detail.html'
context_object_name = 'template'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
template = AppointmentTemplate.objects.filter(tenant=tenant)
return template
class AppointmentTemplateCreateView(LoginRequiredMixin, CreateView):
"""
Create new appointment template.
"""
model = AppointmentTemplate
form_class = AppointmentTemplateForm
template_name = 'appointments/templates/appointment_template_form.html'
permission_required = 'appointments.add_appointmenttemplate'
success_url = reverse_lazy('appointments:appointment_template_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
# Set tenant
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log template creation
AuditLogger.log_event(
tenant=form.instance.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Appointment Template',
description=f'Created template: {self.object.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Appointment template "{self.object.name}" created successfully.')
return response
class AppointmentTemplateUpdateView(LoginRequiredMixin, UpdateView):
"""
Update appointment template.
"""
model = AppointmentTemplate
form_class = AppointmentTemplateForm
template_name = 'appointments/templates/appointment_template_form.html'
permission_required = 'appointments.change_appointmenttemplate'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
return AppointmentTemplate.objects.filter(tenant=tenant)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
return reverse('appointments:appointment_template_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log template update
AuditLogger.log_event(
tenant=self.object.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Appointment Template',
description=f'Updated template: {self.object.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Appointment template "{self.object.name}" updated successfully.')
return response
class AppointmentTemplateDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete appointment template.
"""
model = AppointmentTemplate
template_name = 'appointments/templates/appointment_template_confirm_delete.html'
permission_required = 'appointments.delete_appointmenttemplate'
success_url = reverse_lazy('appointments:appointment_template_list')
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
return AppointmentTemplate.objects.filter(tenant=tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
template_name = self.object.name
# Log template deletion
AuditLogger.log_event(
tenant=getattr(request, 'tenant', None),
event_type='DELETE',
event_category='APPOINTMENT_MANAGEMENT',
action='Delete Appointment Template',
description=f'Deleted template: {template_name}',
user=request.user,
content_object=self.object,
request=request
)
messages.success(request, f'Appointment template "{template_name}" deleted successfully.')
return super().delete(request, *args, **kwargs)
class WaitingListView(LoginRequiredMixin, ListView):
"""
List view for waiting list entries.
"""
model = WaitingList
template_name = 'appointments/waiting_list/waiting_list.html'
context_object_name = 'waiting_list'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
queryset = WaitingList.objects.filter(
tenant=tenant
).select_related(
'patient', 'department', 'provider', 'scheduled_appointment'
).order_by('priority', 'urgency_score', 'created_at')
# Apply filters
form = WaitingListFilterForm(
self.request.GET,
tenant=tenant
)
if form.is_valid():
if form.cleaned_data.get('department'):
queryset = queryset.filter(department=form.cleaned_data['department'])
if form.cleaned_data.get('specialty'):
queryset = queryset.filter(specialty=form.cleaned_data['specialty'])
if form.cleaned_data.get('priority'):
queryset = queryset.filter(priority=form.cleaned_data['priority'])
if form.cleaned_data.get('status'):
queryset = queryset.filter(status=form.cleaned_data['status'])
if form.cleaned_data.get('provider'):
queryset = queryset.filter(provider=form.cleaned_data['provider'])
if form.cleaned_data.get('date_from'):
queryset = queryset.filter(created_at__date__gte=form.cleaned_data['date_from'])
if form.cleaned_data.get('date_to'):
queryset = queryset.filter(created_at__date__lte=form.cleaned_data['date_to'])
if form.cleaned_data.get('urgency_min'):
queryset = queryset.filter(urgency_score__gte=form.cleaned_data['urgency_min'])
if form.cleaned_data.get('urgency_max'):
queryset = queryset.filter(urgency_score__lte=form.cleaned_data['urgency_max'])
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter_form'] = WaitingListFilterForm
context['bulk_action_form'] = WaitingListBulkActionForm
# Statistics
waiting_list = self.get_queryset()
context['stats'] = {
'total': waiting_list.count(),
'active': waiting_list.filter(status='ACTIVE').count(),
'contacted': waiting_list.filter(status='CONTACTED').count(),
'urgent': waiting_list.filter(priority__in=['URGENT', 'STAT', 'EMERGENCY']).count(),
# 'avg_wait_days': waiting_list.aggregate(
# avg_days=Avg('created_at')
# )['avg_days'] or 0,
}
return context
class WaitingListDetailView(LoginRequiredMixin, DetailView):
"""
Detail view for waiting list entry.
"""
model = WaitingList
template_name = 'appointments/waiting_list/waiting_list_detail.html'
context_object_name = 'entry'
def get_queryset(self):
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'tenant', None)
).select_related(
'patient', 'department', 'provider', 'scheduled_appointment',
'created_by', 'removed_by'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get contact logs
context['contact_logs'] = self.object.contact_logs.all().order_by('-contact_date')
# Contact log form
context['contact_form'] = WaitingListContactLogForm()
# Calculate position and wait time
self.object.update_position()
context['estimated_wait_time'] = self.object.estimate_wait_time()
return context
class WaitingListCreateView(LoginRequiredMixin, CreateView):
"""
Create view for waiting list entry.
"""
model = WaitingList
form_class = WaitingListForm
template_name = 'appointments/waiting_list/waiting_list_form.html'
success_url = reverse_lazy('appointments:waiting_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = self.request.user.tenant
return kwargs
def form_valid(self, form):
form.instance.tenant = self.request.user.tenant
form.instance.created_by = self.request.user
response = super().form_valid(form)
self.object.update_position()
self.object.estimated_wait_time = self.object.estimate_wait_time()
self.object.save(update_fields=['position', 'estimated_wait_time'])
messages.success(
self.request,
f"Patient {self.object.patient.get_full_name()} has been added to the waiting list."
)
return response
class WaitingListUpdateView(LoginRequiredMixin, UpdateView):
"""
Update view for waiting list entry.
"""
model = WaitingList
form_class = WaitingListForm
template_name = 'appointments/waiting_list/waiting_list_form.html'
def get_queryset(self):
tenant = self.request.user.tenant
return WaitingList.objects.filter(
tenant=tenant
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request.user, 'tenant', None)
return kwargs
def get_success_url(self):
return reverse('appointments:waiting_list_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
# Update position if priority or urgency changed
old_priority = WaitingList.objects.get(pk=self.object.pk).priority
old_urgency = WaitingList.objects.get(pk=self.object.pk).urgency_score
response = super().form_valid(form)
if (form.instance.priority != old_priority or
form.instance.urgency_score != old_urgency):
self.object.update_position()
self.object.estimated_wait_time = self.object.estimate_wait_time()
self.object.save(update_fields=['position', 'estimated_wait_time'])
messages.success(self.request, "Waiting list entry has been updated.")
return response
class WaitingListDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete view for waiting list entry.
"""
model = WaitingList
template_name = 'appointments/waiting_list/waiting_list_confirm_delete.html'
success_url = reverse_lazy('appointments:waiting_list')
def get_queryset(self):
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'tenant', None)
)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
patient_name = self.object.patient.get_full_name()
# Mark as removed instead of deleting
self.object.status = 'CANCELLED'
self.object.removal_reason = 'PROVIDER_CANCELLED'
self.object.removed_at = timezone.now()
self.object.removed_by = request.user
self.object.save()
messages.success(
request,
f"Waiting list entry for {patient_name} has been cancelled."
)
return redirect(self.success_url)
class SchedulingCalendarView(LoginRequiredMixin, ListView):
"""
Calendar view for scheduling appointments.
"""
model = AppointmentRequest
template_name = 'appointments/scheduling_calendar.html'
context_object_name = 'appointments'
paginate_by = 20
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['appointments'] = AppointmentRequest.objects.filter(
tenant=self.request.user.tenant,
status='SCHEDULED'
).select_related('patient', 'provider').order_by('-scheduled_datetime')
return context
class TelemedicineView(LoginRequiredMixin, ListView):
"""
Telemedicine appointments view.
"""
model = TelemedicineSession
template_name = 'appointments/telemedicine/telemedicine.html'
context_object_name = 'sessions'
paginate_by = 20
def get_queryset(self):
return TelemedicineSession.objects.filter(
appointment__tenant=self.request.user.tenant
).order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['telemedicine_appointments'] = AppointmentRequest.objects.filter(
tenant=self.request.user.tenant,
appointment_type='TELEMEDICINE'
)
return context
class SmartSchedulingView(LoginRequiredMixin, TemplateView):
"""
Smart scheduling interface view.
"""
template_name = 'appointments/scheduling/smart_scheduling.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
if tenant:
# Get active providers
context['providers'] = User.objects.filter(
tenant=tenant,
is_active=True,
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER']
).order_by('last_name', 'first_name')
# Get appointment types
context['appointment_types'] = AppointmentTemplate.objects.filter(
tenant=tenant,
is_active=True
).order_by('specialty', 'name')
# Get recent patients
context['recent_patients'] = PatientProfile.objects.filter(
tenant=tenant,
is_active=True
).order_by('-updated_at')[:20]
return context
class AdvancedQueueManagementView(LoginRequiredMixin, DetailView):
"""
Advanced queue management interface with real-time updates.
Main interface for Phase 11 queue management.
"""
model = WaitingQueue
template_name = 'appointments/queue/advanced_queue_management.html'
context_object_name = 'queue'
def get_queryset(self):
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
return WaitingQueue.objects.filter(tenant=tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
queue = self.get_object()
from appointments.queue import AdvancedQueueEngine
engine = AdvancedQueueEngine(queue)
# Get queue status
context['queue_status'] = engine.get_queue_status()
# Get queue configuration
from appointments.models import QueueConfiguration
config, created = QueueConfiguration.objects.get_or_create(queue=queue)
context['queue_config'] = config
# Get available patients for adding to queue
context['available_patients'] = PatientProfile.objects.filter(
tenant=self.request.user.tenant,
is_active=True
).order_by('last_name', 'first_name')[:50]
# Get available providers
context['available_providers'] = User.objects.filter(
tenant=self.request.user.tenant,
is_active=True,
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER']
).order_by('last_name', 'first_name')
return context
@login_required
def add_contact_log(request, pk):
"""
Add contact log entry for waiting list.
"""
entry = get_object_or_404(
WaitingList,
pk=pk,
tenant=getattr(request.user, 'tenant', None)
)
if request.method == 'POST':
form = WaitingListContactLogForm(request.POST)
if form.is_valid():
contact_log = form.save(commit=False)
contact_log.waiting_list_entry = entry
contact_log.contacted_by = request.user
contact_log.save()
# Update waiting list entry
entry.last_contacted = timezone.now()
entry.contact_attempts += 1
if contact_log.appointment_offered:
entry.appointments_offered += 1
entry.last_offer_date = timezone.now()
if contact_log.patient_response == 'DECLINED':
entry.appointments_declined += 1
elif contact_log.patient_response == 'ACCEPTED':
entry.status = 'SCHEDULED'
if contact_log.contact_outcome == 'SUCCESSFUL':
entry.status = 'CONTACTED'
entry.save()
messages.success(request, "Contact log has been added.")
if request.headers.get('HX-Request'):
return render(request, 'appointments/partials/contact_log_list.html', {
'contact_logs': entry.contact_logs.all().order_by('-contact_date')
})
else:
messages.error(request, "Please correct the errors below.")
return redirect('appointments:waiting_list_detail', pk=pk)
@login_required
def waiting_list_bulk_action(request):
"""
Handle bulk actions on waiting list entries.
"""
if request.method == 'POST':
form = WaitingListBulkActionForm(
request.POST,
tenant=getattr(request.user, 'tenant', None)
)
if form.is_valid():
action = form.cleaned_data['action']
entry_ids = request.POST.getlist('selected_entries')
if not entry_ids:
messages.error(request, "No entries selected.")
return redirect('appointments:waiting_list')
entries = WaitingList.objects.filter(
id__in=entry_ids,
tenant=getattr(request.user, 'tenant', None)
)
if action == 'contact':
entries.update(
status='CONTACTED',
last_contacted=timezone.now(),
contact_attempts=F('contact_attempts') + 1
)
messages.success(request, f"{entries.count()} entries marked as contacted.")
elif action == 'cancel':
entries.update(
status='CANCELLED',
removal_reason='PROVIDER_CANCELLED',
removed_at=timezone.now(),
removed_by=request.user
)
messages.success(request, f"{entries.count()} entries cancelled.")
elif action == 'update_priority':
new_priority = form.cleaned_data.get('new_priority')
if new_priority:
entries.update(priority=new_priority)
# Update positions for affected entries
for entry in entries:
entry.update_position()
messages.success(request, f"{entries.count()} entries priority updated.")
elif action == 'transfer_provider':
transfer_provider = form.cleaned_data.get('transfer_provider')
if transfer_provider:
entries.update(provider=transfer_provider)
messages.success(request, f"{entries.count()} entries transferred.")
elif action == 'export':
# Export functionality would be implemented here
messages.info(request, "Export functionality coming soon.")
return redirect('appointments:waiting_list')
@login_required
def waiting_list_stats(request):
"""
HTMX endpoint for waiting list statistics.
"""
tenant = getattr(request.user, 'tenant', None)
if not tenant:
return JsonResponse({'error': 'No tenant'})
waiting_list = WaitingList.objects.filter(tenant=tenant)
stats = {
'total': waiting_list.count(),
'active': waiting_list.filter(status='ACTIVE').count(),
'contacted': waiting_list.filter(status='CONTACTED').count(),
'scheduled': waiting_list.filter(status='SCHEDULED').count(),
'urgent': waiting_list.filter(priority__in=['URGENT', 'STAT', 'EMERGENCY']).count(),
'overdue_contact': sum(1 for entry in waiting_list.filter(status='ACTIVE') if entry.is_overdue_contact),
# 'avg_wait_days': int(waiting_list.aggregate(
# avg_days=Avg(timezone.now().date() - F('created_at__date'))
# )['avg_days'] or 0),
}
return JsonResponse(stats)
@login_required
def appointment_search(request):
"""
HTMX view for appointment search.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
search_query = request.GET.get('search', '')
queryset = AppointmentRequest.objects.filter(tenant=tenant)
if search_query:
queryset = queryset.filter(
Q(patient__first_name__icontains=search_query) |
Q(patient__last_name__icontains=search_query) |
Q(provider__first_name__icontains=search_query) |
Q(provider__last_name__icontains=search_query) |
Q(reason__icontains=search_query)
)
appointments = queryset.order_by('-scheduled_datetime')[:20]
return render(request, 'appointments/partials/appointment_list.html', {
'appointments': appointments
})
@login_required
def appointment_stats(request):
"""
HTMX view for appointment statistics.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
today = timezone.now().date()
# Calculate appointment statistics
stats = {
'total_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
).count(),
'total_appointments_today': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today
).count(),
'pending_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='PENDING'
).count(),
'completed_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='COMPLETED'
).count(),
'cancelled_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='CANCELLED'
).count(),
'telemedicine_sessions': TelemedicineSession.objects.filter(
appointment__tenant=tenant,
appointment__scheduled_datetime__date=today
).count(),
'active_queues': WaitingQueue.objects.filter(
tenant=tenant,
is_active=True
).count(),
'total_queue_entries': QueueEntry.objects.filter(
queue__tenant=tenant,
status='WAITING'
).count(),
}
return render(request, 'appointments/partials/appointment_stats.html', {'stats': stats})
@login_required
def available_slots(request):
tenant = request.user.tenant
if not tenant:
return render(request, 'appointments/partials/available_slots.html', status=400)
provider_id = request.GET.get('new_provider')
date_str = request.GET.get('new_date')
exclude_id = request.GET.get('exclude_appointment')
if not provider_id or not date_str:
return render(request, 'appointments/partials/available_slots.html', status=400)
try:
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
return render(request, 'appointments/partials/available_slots.html', status=400)
provider = get_object_or_404(User, pk=provider_id)
slots = SlotAvailability.objects.filter(
provider=provider,
provider__tenant=tenant,
date=selected_date,
).order_by('start_time')
current_excluded = slots.exclude(pk=exclude_id)
return render(request, 'appointments/partials/available_slots.html', {'slots': current_excluded}, status=200)
@login_required
def queue_status(request, queue_id):
"""
HTMX view for queue status.
Shows queue entries plus aggregated stats with DB-side wait calculations.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
queue_entries = (
QueueEntry.objects
.filter(queue=queue)
.annotate(
wait_duration=Case(
When(status='WAITING', then=Now() - F('joined_at')),
When(served_at__isnull=False, then=F('served_at') - F('joined_at')),
default=Value(None),
output_field=DurationField(),
),
)
.annotate(
wait_minutes=Case(
When(wait_duration__isnull=False,
then=ExpressionWrapper(
F('wait_duration') / Value(timedelta(minutes=1)),
output_field=FloatField()
)),
default=Value(None),
output_field=FloatField(),
),
waiting_rank=Case(
When(status='WAITING', then=F('queue_position')),
default=Value(None), output_field=IntegerField()
),
)
.select_related('assigned_provider', 'patient', 'appointment')
.order_by('queue_position', 'updated_at')
)
# Aggregates & stats
total_entries = queue_entries.count()
waiting_entries = queue_entries.filter(status='WAITING').count()
called_entries = queue_entries.filter(status='CALLED').count()
in_service_entries = queue_entries.filter(status='IN_SERVICE').count()
completed_entries = queue_entries.filter(status='COMPLETED').count()
avg_completed_wait = (
queue_entries
.filter(status='COMPLETED')
.aggregate(avg_wait=Avg('wait_minutes'))
.get('avg_wait') or 0
)
stats = {
'total_entries': total_entries,
'waiting_entries': waiting_entries,
'called_entries': called_entries,
'in_service_entries': in_service_entries,
'completed_entries': completed_entries,
# Average from COMPLETED cases only (rounded to 1 decimal)
'average_wait_time_minutes': round(avg_completed_wait, 1),
# Quick estimate based on queue config
'estimated_queue_wait_minutes': waiting_entries * queue.average_service_time_minutes,
}
return render(request, 'appointments/partials/queue_status.html', {
'queue': queue,
'queue_entries': queue_entries, # each has .wait_minutes
'stats': stats,
})
@login_required
def calendar_appointments(request):
"""
HTMX view for calendar appointments.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
date_str = request.GET.get('date')
provider_id = request.GET.get('provider_id')
if not date_str:
date_str = timezone.now().date().strftime('%Y-%m-%d')
try:
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
selected_date = timezone.now().date()
# Get appointments for the selected date
queryset = AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=selected_date
)
if provider_id:
queryset = queryset.filter(provider__id=provider_id)
appointments = queryset.order_by('scheduled_datetime')
# providers = queryset.order_by('provider__first_name')
return render(request, 'appointments/partials/calendar_appointments.html', {
'appointments': appointments,
'selected_date': selected_date,
# 'providers': providers
})
@login_required
def confirm_appointment(request, pk):
"""
Confirm an appointment.
"""
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:appointment_request_list')
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
appointment.status = 'CONFIRMED'
appointment.save()
# Log confirmation
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Confirm Appointment',
description=f'Confirmed appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
messages.success(request, f'Appointment for {appointment.patient} confirmed successfully.')
return redirect('appointments:appointment_request_detail', pk=pk)
@login_required
def reschedule_appointment(request, pk):
tenant = request.user.tenant
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
providers = User.objects.filter(
tenant=tenant, employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
).order_by('last_name', 'first_name')
if request.method == 'POST':
new_date_str = request.POST.get('new_date')
new_time_str = request.POST.get('new_time')
new_provider_id = request.POST.get('new_provider') or None
reschedule_reason = request.POST.get('reschedule_reason') or ''
reschedule_notes = request.POST.get('reschedule_notes') or ''
notify_patient = bool(request.POST.get('notify_patient'))
# Validate and parse
try:
new_date = datetime.strptime(new_date_str, '%Y-%m-%d').date()
new_time = datetime.strptime(new_time_str, '%H:%M').time()
except (TypeError, ValueError):
messages.error(request, 'Please provide a valid date and time.')
return render(request, 'appointments/reschedule_appointment.html', {
'appointment': appointment,
'providers': providers,
'today': timezone.localdate(), # for min attr if you use it
})
appointment.scheduled_date = new_date
appointment.scheduled_time = new_time
if new_provider_id:
appointment.provider_id = new_provider_id
appointment.status = 'RESCHEDULED'
appointment.reschedule_reason = reschedule_reason
appointment.reschedule_notes = reschedule_notes
appointment.save()
# optionally send notifications if notify_patient is True
messages.success(request, 'Appointment has been rescheduled.')
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Reschedule Appointment',
description=f'Rescheduled appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
return redirect('appointments:appointment_detail', pk=appointment.pk)
return render(request, 'appointments/reschedule_appointment.html', {
'appointment': appointment,
'providers': providers,
'today': timezone.localdate(),
})
@login_required
def cancel_appointment(request, pk):
"""
Complete an appointment.
"""
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:appointment_request_list')
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
if appointment.status == 'SCHEDULED':
appointment.status = 'CANCELLED'
# appointment.actual_end_time = timezone.now()
appointment.save()
# Log completion
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Appointment',
description=f'Cancelled appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
return redirect('appointments:appointment_detail', pk=pk)
return render(request, 'appointments/cancel_appointment.html', {
'appointment': appointment,
})
@login_required
def start_appointment(request, pk):
"""
Start an appointment.
"""
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:appointment_request_list')
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
appointment.status = 'IN_PROGRESS'
appointment.actual_start_time = timezone.now()
appointment.save()
# Log start
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Start Appointment',
description=f'Started appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
messages.success(request, f'Appointment for {appointment.patient} started successfully.')
return redirect('appointments:appointment_request_detail', pk=pk)
@login_required
def complete_appointment(request, pk):
"""
Complete an appointment.
"""
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:appointment_request_list')
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
appointment.status = 'COMPLETED'
appointment.actual_end_time = timezone.now()
appointment.save()
# Log completion
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Complete Appointment',
description=f'Completed appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
messages.success(request, f'Appointment for {appointment.patient} completed successfully.')
return redirect('appointments:appointment_request_detail', pk=pk)
@login_required
def next_in_queue(request, queue_id):
"""
Call next patient in queue.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
# Get next waiting entry
next_entry = QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).order_by('queue_position', 'called_at').first()
if next_entry:
next_entry.status = 'IN_SERVICE'
next_entry.called_at = timezone.now()
next_entry.save()
messages.success(request, f"Patient has been called in for appointment.")
# Log queue progression
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Call Next in Queue',
description=f'Called next patient: {next_entry.patient} in queue: {queue.name}',
user=request.user,
content_object=next_entry,
request=request
)
return redirect('appointments:waiting_queue_detail', pk=queue.pk)
else:
messages.error(request, f"No more patients in queue.")
return redirect('appointments:waiting_queue_detail', pk=queue.pk)
@login_required
def check_in_patient(request, appointment_id):
"""
Check in a patient for their appointment.
"""
appointment = get_object_or_404(AppointmentRequest,
pk=appointment_id,
tenant=request.user.tenant
)
appointment.status = 'CHECKED_IN'
appointment.save()
messages.success(request, f"Patient {appointment.patient} has been checked in.")
return redirect('appointments:waiting_queue_list')
@login_required
def complete_queue_entry(request, pk):
"""
Complete a queue entry.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue_entry = get_object_or_404(
QueueEntry,
pk=pk,
queue__tenant=tenant
)
queue_entry.status = 'COMPLETED'
queue_entry.completed_at = timezone.now()
# Calculate actual wait time
if queue_entry.called_at:
wait_time = (queue_entry.called_at - queue_entry.joined_at).total_seconds() / 60
queue_entry.actual_wait_time_minutes = int(wait_time)
queue_entry.save()
messages.success(request, f"Queue entry {queue_entry.pk} completed successfully.")
# Log completion
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Complete Queue Entry',
description=f'Completed queue entry for: {queue_entry.patient}',
user=request.user,
content_object=queue_entry,
request=request
)
return redirect('appointments:waiting_queue_detail', pk=queue_entry.queue.pk)
@login_required
def start_telemedicine_session(request, pk):
"""
Start a telemedicine session.
"""
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:telemedicine_session_list')
session = get_object_or_404(
TelemedicineSession,
pk=pk,
appointment__tenant=tenant
)
session.status = 'IN_PROGRESS'
session.start_time = timezone.now()
session.save()
# Update appointment status
session.appointment.status = 'IN_PROGRESS'
session.appointment.save()
# Log session start
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Start Telemedicine Session',
description=f'Started telemedicine session for: {session.appointment}',
user=request.user,
content_object=session,
request=request
)
messages.success(request, f'Telemedicine session started successfully.')
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
@login_required
def end_telemedicine_session(request, pk):
"""
End a telemedicine session.
"""
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:telemedicine_session_list')
session = get_object_or_404(
TelemedicineSession,
pk=pk,
appointment__tenant=tenant
)
session.status = 'COMPLETED'
session.end_time = timezone.now()
session.save()
# Update appointment status
session.appointment.status = 'COMPLETED'
session.appointment.save()
# Log session end
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='End Telemedicine Session',
description=f'Ended telemedicine session for: {session.appointment}',
user=request.user,
content_object=session,
request=request
)
messages.success(request, 'Telemedicine session ended successfully.')
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
@login_required
def cancel_telemedicine_session(request, pk):
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:telemedicine_session_list')
session = get_object_or_404(TelemedicineSession, pk=pk)
session.status = 'CANCELLED'
session.save()
# Update appointment status
session.appointment.status = 'CANCELLED'
session.appointment.save()
# Log session start
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Telemedicine Session',
description='Cancelled telemedicine session',
user=request.user,
content_object=session,
request=request
)
messages.success(request, 'Telemedicine session cancelled successfully.')
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
@login_required
def calendar_view(request):
"""Renders the calendar page"""
return render(request, "appointments/calendar.html")
def queue_display_view(request, pk):
"""
Public queue display view for patients to see their waiting numbers.
No login required - this is a public display screen.
"""
queue = get_object_or_404(WaitingQueue, pk=pk)
# Get current patient being served
current_patient = QueueEntry.objects.filter(
queue=queue,
status='IN_SERVICE'
).order_by('called_at').first()
# Get waiting patients
waiting_patients = QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).order_by('queue_position', 'joined_at')[:20] # Show max 20 waiting
# Calculate statistics
today = timezone.now().date()
stats = {
'total_waiting': QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).count(),
'avg_wait_time': queue.average_service_time_minutes,
'served_today': QueueEntry.objects.filter(
queue=queue,
status='COMPLETED',
served_at__date=today
).count(),
}
return render(request, 'appointments/queue/queue_display.html', {
'queue': queue,
'current_patient': current_patient,
'waiting_patients': waiting_patients,
'stats': stats,
})
def queue_monitor_view(request, pk):
"""
Public queue monitor view - large screen display for waiting room.
No login required - this is a public display screen (TV/monitor).
Shows current patient being served and next patients in queue.
Auto-refreshes every 30 seconds and supports WebSocket updates.
"""
queue = get_object_or_404(WaitingQueue, pk=pk)
# Get current patient being served
current_patient = QueueEntry.objects.filter(
queue=queue,
status='IN_SERVICE'
).order_by('called_at').first()
# Get waiting patients (next 10 in queue)
waiting_patients = QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).select_related('patient').order_by('queue_position', 'joined_at')[:10]
# Add wait time display for each patient
for patient in waiting_patients:
if patient.joined_at:
wait_duration = timezone.now() - patient.joined_at
minutes = int(wait_duration.total_seconds() / 60)
if minutes < 60:
patient.wait_time_display = f"{minutes} min"
else:
hours = minutes // 60
mins = minutes % 60
patient.wait_time_display = f"{hours}h {mins}m"
# Calculate statistics
today = timezone.now().date()
stats = {
'total_waiting': QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).count(),
'avg_wait_time': queue.average_service_time_minutes,
'served_today': QueueEntry.objects.filter(
queue=queue,
status='COMPLETED',
served_at__date=today
).count(),
}
return render(request, 'appointments/queue/queue_monitor.html', {
'queue': queue,
'current_patient': current_patient,
'waiting_patients': waiting_patients,
'stats': stats,
})
@login_required
@require_GET
def calendar_events(request):
"""
FullCalendar event feed (GET /calendar/events?start=..&end=..[&provider_id=&status=...])
FullCalendar sends ISO timestamps; we return a list of event dicts.
"""
STATUS_COLORS = {
"PENDING": {"bg": "#f59c1a", "border": "#d08916"},
"CONFIRMED": {"bg": "#49b6d6", "border": "#3f9db9"},
"CHECKED_IN": {"bg": "#348fe2", "border": "#2c79bf"},
"IN_PROGRESS": {"bg": "#00acac", "border": "#009494"},
"COMPLETED": {"bg": "#32a932", "border": "#298a29"},
"CANCELLED": {"bg": "#ff5b57", "border": "#d64d4a"},
"NO_SHOW": {"bg": "#6c757d", "border": "#5a636b"},
}
tenant = request.user.tenant
if not tenant:
return JsonResponse([], safe=False)
start = request.GET.get("start")
end = request.GET.get("end")
provider_id = request.GET.get("provider_id")
status = request.GET.get("status")
if not start or not end:
return HttpResponseBadRequest("Missing start/end")
# Parse (FullCalendar uses ISO 8601)
# They can include timezone; parse_datetime handles offsets.
start_dt = parse_datetime(start)
end_dt = parse_datetime(end)
if not start_dt or not end_dt:
return HttpResponseBadRequest("Invalid start/end")
qs = AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__gte=start_dt,
scheduled_datetime__lt=end_dt,
).select_related("patient", "provider")
if provider_id:
qs = qs.filter(provider_id=provider_id)
if status:
qs = qs.filter(status=status)
events = []
for appt in qs:
color = STATUS_COLORS.get(appt.status, {"bg": "#495057", "border": "#3e444a"})
title = f"{appt.patient.get_full_name()}{appt.get_appointment_type_display()}"
if appt.is_telemedicine:
title = "📹 " + title
# If you store end time separately, use it; else estimate duration (e.g., 30 min)
end_time = getattr(appt, "end_datetime", None)
if not end_time:
end_time = appt.scheduled_datetime + timedelta(minutes=getattr(appt, "duration_minutes", 30))
events.append({
"id": str(appt.pk),
"title": title,
"start": appt.scheduled_datetime.isoformat(),
"end": end_time.isoformat(),
"backgroundColor": color["bg"],
"borderColor": color["border"],
"textColor": "#fff",
"extendedProps": {
"status": appt.status,
"provider": appt.provider.get_full_name() if appt.provider_id else "",
"chief_complaint": (appt.chief_complaint or "")[:120],
"telemedicine": appt.is_telemedicine,
},
})
return JsonResponse(events, safe=False)
@login_required
def appointment_detail_card(request, pk):
tenant = request.user.tenant
"""HTMX partial with appointment quick details for the sidebar/modal."""
appt = get_object_or_404(AppointmentRequest.objects.select_related("patient","provider"), pk=pk, tenant=tenant)
return render(request, "appointments/partials/appointment_detail_card.html", {"appointment": appt})
@login_required
@permission_required("appointments.change_appointment")
@require_POST
def appointment_reschedule(request, pk):
"""
Handle drag/drop or resize from FullCalendar.
Expect JSON: {"start":"...", "end":"..."} ISO strings (local/offset).
"""
tenant = request.user.tenant
appt = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
try:
data = request.POST if request.content_type == "application/x-www-form-urlencoded" else request.json()
except Exception:
data = {}
start = data.get("start")
end = data.get("end")
start_dt = parse_datetime(start) if start else None
end_dt = parse_datetime(end) if end else None
if not start_dt or not end_dt:
return HttpResponseBadRequest("Invalid start/end")
appt.scheduled_datetime = start_dt
if hasattr(appt, "end_datetime"):
appt.end_datetime = end_dt
elif hasattr(appt, "duration_minutes"):
appt.duration_minutes = int((end_dt - start_dt).total_seconds() // 60)
appt.save(update_fields=["scheduled_datetime"] + (["end_datetime"] if hasattr(appt,"end_datetime") else ["duration_minutes"]))
return JsonResponse({"ok": True})
@login_required
def provider_availability(request):
"""
HTMX view for provider availability.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
provider_id = request.GET.get('provider_id')
date_str = request.GET.get('date')
if not provider_id:
return render(request, 'appointments/partials/provider_availability.html', {
'availability': None,
'provider': None,
'selected_date': None
})
try:
provider = User.objects.get(id=provider_id, tenant=tenant)
except User.DoesNotExist:
return JsonResponse({'error': 'Provider not found'}, status=404)
if not date_str:
date_str = timezone.now().date().strftime('%Y-%m-%d')
try:
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
selected_date = timezone.now().date()
# Get availability for the selected date
start_of_day = timezone.make_aware(datetime.combine(selected_date, datetime.min.time()))
end_of_day = timezone.make_aware(datetime.combine(selected_date, datetime.max.time()))
# Regular availability slots
availability = SlotAvailability.objects.filter(
tenant=tenant,
provider=provider,
start_time__lt=end_of_day,
end_time__gt=start_of_day,
is_available=True
).order_by('start_time')
# Recurring availability (based on day of week)
day_of_week = selected_date.weekday() # 0-6, Monday is 0
recurring_availability = SlotAvailability.objects.filter(
tenant=tenant,
provider=provider,
is_recurring=True,
day_of_week=day_of_week,
is_available=True
)
# Get appointments for this provider on this date
appointments = AppointmentRequest.objects.filter(
tenant=tenant,
provider=provider,
scheduled_datetime__date=selected_date,
status__in=['SCHEDULED', 'CONFIRMED', 'CHECKED_IN', 'IN_PROGRESS']
).select_related('patient').order_by('scheduled_datetime')
return render(request, 'appointments/partials/provider_availability.html', {
'availability': availability,
'recurring_availability': recurring_availability,
'appointments': appointments,
'provider': provider,
'selected_date': selected_date
})
@login_required
def add_to_queue(request, queue_id):
"""
HTMX view for adding a patient to the queue.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, queue_id=queue_id, tenant=tenant)
if request.method == 'POST':
form = QueueEntryForm(request.POST, user=request.user)
if form.is_valid():
entry = form.save(commit=False)
entry.queue = queue
entry.save()
return redirect('appointments:queue_status', queue_id=queue_id)
else:
form = QueueEntryForm(
user=request.user,
initial={'queue': queue, 'priority': 'ROUTINE'}
)
return render(request, 'appointments/partials/add_to_queue.html', {
'form': form,
'queue': queue
})
@login_required
def call_next_patient(request, queue_id):
"""
HTMX view for calling the next patient from the queue.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, queue_id=queue_id, tenant=tenant)
# Check if the user has permission
if not request.user.has_perm('appointments.change_queueentry'):
return HttpResponseForbidden("Permission denied")
# Get the next patient
next_entry = queue.get_next_patient()
if next_entry:
next_entry.mark_as_called()
# Return the updated queue status
return redirect('appointments:queue_status', queue_id=queue_id)
else:
# No patients waiting
return render(request, 'appointments/partials/no_patients_waiting.html', {
'queue': queue
})
@login_required
def update_entry_status(request, entry_id, status):
"""
HTMX view for updating a queue entry status.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
entry = get_object_or_404(QueueEntry, entry_id=entry_id, queue__tenant=tenant)
# Check if the user has permission
if not request.user.has_perm('appointments.change_queueentry'):
return HttpResponseForbidden("Permission denied")
# Update the status
if status == 'called':
entry.mark_as_called()
elif status == 'in_progress':
entry.mark_as_in_progress()
elif status == 'completed':
entry.mark_as_completed()
elif status == 'no_show':
entry.mark_as_no_show()
elif status == 'cancelled':
entry.mark_as_cancelled()
elif status == 'removed':
entry.mark_as_removed()
# Return the updated queue status
return redirect('appointments:queue_status', queue_id=entry.queue.queue_id)
@login_required
def check_in_appointment(request, pk):
"""
View for checking in a patient for an appointment.
"""
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, _("No tenant found. Please contact an administrator."))
return redirect('core:dashboard')
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
# Check if the user has permission
if not request.user.has_perm('appointments.change_appointmentrequest'):
messages.error(request, _("You don't have permission to check in patients."))
return redirect('appointments:appointment_detail', pk=appointment.pk)
# Check if the appointment can be checked in
if appointment.status not in ['SCHEDULED', 'CONFIRMED']:
messages.error(request, _("This appointment cannot be checked in."))
return redirect('appointments:appointment_detail', pk=appointment.pk)
# Check in the appointment
appointment.check_in()
messages.success(request, _("Patient has been checked in."))
return redirect('appointments:appointment_detail', pk=appointment.pk)
@login_required
def create_telemedicine_session(request, appointment_id):
"""
HTMX view for creating a telemedicine session for an appointment.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
appointment = get_object_or_404(
AppointmentRequest,
appointment_id=appointment_id,
tenant=tenant
)
# Check if appointment is of type telemedicine
if appointment.appointment_type != 'TELEMEDICINE':
return JsonResponse({'error': 'Not a telemedicine appointment'}, status=400)
# Check if a session already exists
if hasattr(appointment, 'telemedicine_session'):
return JsonResponse({'error': 'Session already exists'}, status=400)
if request.method == 'POST':
form = TelemedicineSessionForm(
request.POST,
user=request.user,
appointment=appointment
)
if form.is_valid():
session = form.save(commit=False)
session.tenant = tenant
session.save()
# Link session to appointment
appointment.telemedicine_session = session
appointment.save()
return render(request, 'appointments/partials/telemedicine_session.html', {
'session': session,
'appointment': appointment
})
else:
form = TelemedicineSessionForm(
user=request.user,
appointment=appointment,
initial={
'status': 'SCHEDULED',
'scheduled_start_time': appointment.scheduled_datetime,
'scheduled_end_time': appointment.scheduled_datetime + timedelta(
minutes=appointment.duration_minutes
),
'room_name': f"telehealth-{appointment.appointment_id.hex[:8]}",
'is_recorded': False,
}
)
return render(request, 'appointments/partials/telemedicine_session_form.html', {
'form': form,
'appointment': appointment
})
@login_required
def telemedicine_session_status(request, session_id):
"""
HTMX view for updating telemedicine session status.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
session = get_object_or_404(
TelemedicineSession,
session_id=session_id,
tenant=tenant
)
# Get the requested action
action = request.POST.get('action')
if action == 'provider_joined':
session.mark_provider_joined()
elif action == 'patient_joined':
session.mark_patient_joined()
elif action == 'end_session':
session.end_session()
elif action == 'cancel_session':
session.cancel_session()
return render(request, 'appointments/partials/telemedicine_session.html', {
'session': session,
'appointment': session.appointment
})
@login_required
def find_optimal_slots_view(request):
"""
HTMX view for finding optimal appointment slots.
"""
from appointments.scheduling.smart_scheduler import SmartScheduler
from datetime import datetime, timedelta
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
if request.method == 'POST':
patient_id = request.POST.get('patient_id')
provider_id = request.POST.get('provider_id')
appointment_type_id = request.POST.get('appointment_type_id')
preferred_date = request.POST.get('preferred_date')
duration_minutes = int(request.POST.get('duration_minutes', 30))
# Validate inputs
if not all([patient_id, provider_id, appointment_type_id, preferred_date]):
return JsonResponse({'error': 'Missing required fields'}, status=400)
try:
patient = PatientProfile.objects.get(id=patient_id, tenant=tenant)
provider = User.objects.get(id=provider_id)
appointment_type = AppointmentTemplate.objects.get(id=appointment_type_id, tenant=tenant)
# Parse date and create date range (7 days)
start_date = datetime.strptime(preferred_date, '%Y-%m-%d')
date_range = [start_date + timedelta(days=i) for i in range(7)]
# Find optimal slots
scheduler = SmartScheduler(tenant)
slots = scheduler.find_optimal_slots(
patient=patient,
provider=provider,
appointment_type=appointment_type,
preferred_dates=date_range,
duration_minutes=duration_minutes,
max_results=10
)
return render(request, 'appointments/scheduling/partials/optimal_slots.html', {
'slots': slots,
'patient': patient,
'provider': provider,
'appointment_type': appointment_type
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)
return JsonResponse({'error': 'Invalid request method'}, status=405)
@login_required
def scheduling_analytics_view(request):
"""
View for scheduling analytics dashboard.
"""
from appointments.scheduling.utils import SchedulingAnalytics
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:dashboard')
# Get date range from request or default to last 30 days
days = int(request.GET.get('days', 30))
end_date = timezone.now().date()
start_date = end_date - timedelta(days=days)
# Get provider from request or show all
provider_id = request.GET.get('provider_id')
if provider_id:
provider = get_object_or_404(User, id=provider_id)
providers = [provider]
else:
providers = User.objects.filter(
tenant=tenant,
is_active=True,
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER']
)
# Calculate analytics for each provider
analytics_data = []
for provider in providers:
utilization = SchedulingAnalytics.calculate_provider_utilization(
provider, start_date, end_date
)
no_show_rate = SchedulingAnalytics.calculate_no_show_rate(
provider, start_date, end_date
)
analytics_data.append({
'provider': provider,
'utilization_rate': utilization,
'no_show_rate': no_show_rate
})
return render(request, 'appointments/scheduling/analytics.html', {
'analytics_data': analytics_data,
'start_date': start_date,
'end_date': end_date,
'days': days,
'all_providers': User.objects.filter(
tenant=tenant,
is_active=True,
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER']
).order_by('last_name', 'first_name')
})
@login_required
def check_scheduling_conflicts_view(request):
"""
HTMX view for checking scheduling conflicts.
"""
from appointments.scheduling.utils import ConflictDetector
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
if request.method == 'POST':
provider_id = request.POST.get('provider_id')
start_time = request.POST.get('start_time')
end_time = request.POST.get('end_time')
exclude_appointment_id = request.POST.get('exclude_appointment_id')
if not all([provider_id, start_time, end_time]):
return JsonResponse({'error': 'Missing required fields'}, status=400)
try:
provider = User.objects.get(id=provider_id)
start_dt = datetime.fromisoformat(start_time)
end_dt = datetime.fromisoformat(end_time)
# Check conflicts
conflicts = ConflictDetector.check_conflicts(
provider, start_dt, end_dt, exclude_appointment_id
)
if conflicts:
# Get alternative slots
duration_minutes = int((end_dt - start_dt).total_seconds() / 60)
alternatives = ConflictDetector.suggest_alternative_slots(
provider, start_dt, duration_minutes, count=5
)
return render(request, 'appointments/scheduling/partials/conflicts.html', {
'has_conflict': True,
'conflicts': conflicts,
'alternatives': alternatives,
'provider': provider
})
else:
return render(request, 'appointments/scheduling/partials/conflicts.html', {
'has_conflict': False,
'provider': provider
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)
return JsonResponse({'error': 'Invalid request method'}, status=405)
@login_required
def update_patient_preferences_view(request, patient_id):
"""
View to manually update patient scheduling preferences.
"""
from appointments.scheduling.utils import SchedulingAnalytics
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:dashboard')
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
# Update preferences based on historical data
prefs = SchedulingAnalytics.update_patient_scheduling_preferences(patient)
if prefs:
messages.success(
request,
f'Scheduling preferences updated for {patient.get_full_name()}. '
f'Completion rate: {prefs.completion_rate}%, No-show rate: {prefs.average_no_show_rate}%'
)
else:
messages.info(
request,
f'No appointment history found for {patient.get_full_name()}.'
)
return redirect('patients:patient_detail', pk=patient_id)
@login_required
def scheduling_metrics_dashboard(request):
"""
Dashboard view for scheduling metrics.
"""
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:dashboard')
# Get date range
days = int(request.GET.get('days', 30))
end_date = timezone.now().date()
start_date = end_date - timedelta(days=days)
# Get metrics for the period
metrics = SchedulingMetrics.objects.filter(
tenant=tenant,
date__gte=start_date,
date__lte=end_date
).select_related('provider').order_by('-date')
# Calculate aggregate statistics
total_metrics = metrics.aggregate(
avg_utilization=Avg('utilization_rate'),
avg_no_show=Avg('no_show_rate'),
total_appointments=Count('id')
)
return render(request, 'appointments/scheduling/metrics_dashboard.html', {
'metrics': metrics[:50], # Limit to 50 most recent
'total_metrics': total_metrics,
'start_date': start_date,
'end_date': end_date,
'days': days
})
@login_required
def queue_status_htmx_view(request, queue_id):
"""
HTMX view for real-time queue status updates.
Returns queue statistics and entry list.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
from appointments.queue import AdvancedQueueEngine
engine = AdvancedQueueEngine(queue)
# Get current status
status = engine.get_queue_status()
return render(request, 'appointments/queue/partials/queue_stats.html', {
'queue': queue,
'status': status
})
@login_required
def add_to_queue_htmx_view(request, queue_id):
"""
HTMX view for adding a patient to the queue.
Uses AdvancedQueueEngine for intelligent positioning.
"""
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
if request.method == 'POST':
patient_id = request.POST.get('patient_id')
appointment_id = request.POST.get('appointment_id')
priority_score = float(request.POST.get('priority_score', 1.0))
notes = request.POST.get('notes', '')
if not patient_id:
return JsonResponse({'error': 'Patient ID required'}, status=400)
try:
patient = PatientProfile.objects.get(id=patient_id, tenant=tenant)
appointment = None
if appointment_id:
appointment = AppointmentRequest.objects.get(
id=appointment_id,
tenant=tenant
)
# Use AdvancedQueueEngine to add patient
from appointments.queue import AdvancedQueueEngine
engine = AdvancedQueueEngine(queue)
entry = engine.add_to_queue(
patient=patient,
appointment=appointment,
priority_score=priority_score,
notes=notes
)
# Log the action
AuditLogger.log_event(
tenant=tenant,
event_type='CREATE',
event_category='QUEUE_MANAGEMENT',
action='Add Patient to Queue',
description=f'Added {patient.get_full_name()} to queue {queue.name} at position {entry.queue_position}',
user=request.user,
content_object=entry,
request=request
)
messages.success(
request,
f'{patient.get_full_name()} added to queue at position {entry.queue_position}'
)
# Return updated queue list
return render(request, 'appointments/queue/partials/queue_list.html', {
'queue': queue,
'entries': queue.queue_entries.filter(status='WAITING').order_by('queue_position')
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)
# GET request - show form
patients = PatientProfile.objects.filter(
tenant=tenant,
is_active=True
).order_by('last_name', 'first_name')[:50]
return render(request, 'appointments/queue/partials/add_patient_form.html', {
'queue': queue,
'patients': patients
})
@login_required
def call_next_patient_htmx_view(request, queue_id):
"""
HTMX view for calling the next patient in queue.
Uses AdvancedQueueEngine to get next patient.
"""
tenant = get_tenant_from_request(request)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
from appointments.queue import AdvancedQueueEngine
engine = AdvancedQueueEngine(queue)
# Get next patient
next_patient = engine.get_next_patient()
if next_patient:
# Log the action
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='QUEUE_MANAGEMENT',
action='Call Next Patient',
description=f'Called {next_patient.patient.get_full_name()} from queue {queue.name}',
user=request.user,
content_object=next_patient,
request=request
)
messages.success(
request,
f'Called {next_patient.patient.get_full_name()} - Position {next_patient.queue_position}'
)
return render(request, 'appointments/queue/partials/next_patient.html', {
'queue': queue,
'entry': next_patient
})
else:
messages.info(request, 'No patients waiting in queue')
return render(request, 'appointments/queue/partials/next_patient.html', {
'queue': queue,
'entry': None
})
@login_required
def queue_analytics_view(request, queue_id):
"""
Analytics dashboard for queue performance.
"""
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:dashboard')
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
# Get date range
days = int(request.GET.get('days', 7))
end_date = timezone.now().date()
start_date = end_date - timedelta(days=days)
from appointments.queue import AdvancedQueueEngine
engine = AdvancedQueueEngine(queue)
# Get analytics summary
analytics = engine.get_analytics_summary(days=days)
# Get hourly metrics
from appointments.models import QueueMetrics
hourly_metrics = QueueMetrics.objects.filter(
queue=queue,
date__gte=start_date,
date__lte=end_date
).order_by('date', 'hour')
return render(request, 'appointments/queue/queue_analytics.html', {
'queue': queue,
'analytics': analytics,
'hourly_metrics': hourly_metrics,
'start_date': start_date,
'end_date': end_date,
'days': days
})
@login_required
def queue_metrics_dashboard_view(request):
"""
Dashboard view for all queue metrics across the system.
"""
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:dashboard')
# Get date range
days = int(request.GET.get('days', 7))
end_date = timezone.now().date()
start_date = end_date - timedelta(days=days)
# Get all active queues
queues = WaitingQueue.objects.filter(
tenant=tenant,
is_active=True
)
# Get metrics for each queue
from appointments.models import QueueMetrics
queue_metrics = []
for queue in queues:
metrics = QueueMetrics.objects.filter(
queue=queue,
date__gte=start_date,
date__lte=end_date
).aggregate(
avg_wait_time=Avg('average_wait_time_minutes'),
avg_utilization=Avg('utilization_rate'),
avg_no_show=Avg('no_show_rate'),
total_entries=Count('total_entries'),
total_completed=Count('completed_entries')
)
queue_metrics.append({
'queue': queue,
'metrics': metrics
})
return render(request, 'appointments/queue/metrics_dashboard.html', {
'queue_metrics': queue_metrics,
'start_date': start_date,
'end_date': end_date,
'days': days
})
@login_required
def queue_config_view(request, queue_id):
"""
View for managing queue configuration.
"""
tenant = get_tenant_from_request(request)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:dashboard')
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
from appointments.models import QueueConfiguration
config, created = QueueConfiguration.objects.get_or_create(queue=queue)
if request.method == 'POST':
# Update configuration
config.use_dynamic_positioning = request.POST.get('use_dynamic_positioning') == 'on'
config.priority_weight = float(request.POST.get('priority_weight', 0.5))
config.wait_time_weight = float(request.POST.get('wait_time_weight', 0.3))
config.appointment_time_weight = float(request.POST.get('appointment_time_weight', 0.2))
config.enable_overflow_queue = request.POST.get('enable_overflow_queue') == 'on'
config.overflow_threshold = int(request.POST.get('overflow_threshold', 50))
config.use_historical_data = request.POST.get('use_historical_data') == 'on'
config.default_service_time_minutes = int(request.POST.get('default_service_time_minutes', 15))
config.historical_data_days = int(request.POST.get('historical_data_days', 30))
config.enable_websocket_updates = request.POST.get('enable_websocket_updates') == 'on'
config.update_interval_seconds = int(request.POST.get('update_interval_seconds', 30))
config.load_factor_normal_threshold = float(request.POST.get('load_factor_normal_threshold', 0.5))
config.load_factor_moderate_threshold = float(request.POST.get('load_factor_moderate_threshold', 0.75))
config.load_factor_high_threshold = float(request.POST.get('load_factor_high_threshold', 0.9))
config.auto_reposition_enabled = request.POST.get('auto_reposition_enabled') == 'on'
config.reposition_interval_minutes = int(request.POST.get('reposition_interval_minutes', 15))
config.notify_on_position_change = request.POST.get('notify_on_position_change') == 'on'
config.position_change_threshold = int(request.POST.get('position_change_threshold', 3))
config.save()
# Log configuration update
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='QUEUE_MANAGEMENT',
action='Update Queue Configuration',
description=f'Updated configuration for queue {queue.name}',
user=request.user,
content_object=config,
request=request
)
messages.success(request, f'Queue configuration updated successfully.')
return redirect('appointments:advanced_queue_management', pk=queue_id)
return render(request, 'appointments/queue/queue_config.html', {
'queue': queue,
'config': config
})
@login_required
@permission_required('appointments.change_appointmentrequest')
def no_show_appointment(request, pk):
"""Mark appointment as no-show with documentation."""
tenant = request.user.tenant
appointment = get_object_or_404(
AppointmentRequest,
pk=pk,
tenant=tenant
)
if request.method == 'POST':
# Process no-show form
appointment.status = 'NO_SHOW'
appointment.no_show_time = timezone.now()
appointment.no_show_documented_by = request.user
appointment.no_show_notes = request.POST.get('no_show_notes', '')
appointment.no_show_fee = request.POST.get('no_show_fee', 25.00)
appointment.save()
messages.success(request, 'Appointment marked as no-show.')
return redirect('appointments:appointment_detail', pk=pk)
return render(request, 'appointments/no_show_appointment.html', {
'appointment': appointment
})
class ProviderDailyScheduleView(LoginRequiredMixin, TemplateView):
"""
Provider-facing view showing daily schedule with appointments.
Shows appointments for the logged-in provider for a specific date.
"""
template_name = 'appointments/provider/provider_daily_schedule.html'
def dispatch(self, request, *args, **kwargs):
# Ensure user is a provider (has employee profile with provider role)
if not hasattr(request.user, 'employee_profile'):
messages.error(request, 'You must be a provider to access this page.')
return redirect('core:dashboard')
# Check if user has provider role
provider_roles = ['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT',
'PHARMACIST', 'RADIOLOGIST', 'THERAPIST']
if request.user.employee_profile.role not in provider_roles:
messages.error(request, 'You must be a provider to access this page.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
provider = self.request.user
# Get selected date from query params or use today
date_str = self.request.GET.get('date')
if date_str:
try:
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
selected_date = timezone.now().date()
else:
selected_date = timezone.now().date()
# Get status filter
status_filter = self.request.GET.get('status', '')
# Get appointments for the provider on selected date
appointments = AppointmentRequest.objects.filter(
tenant=tenant,
provider=provider,
scheduled_datetime__date=selected_date
).select_related('patient').order_by('scheduled_datetime')
# Apply status filter if provided
if status_filter:
appointments = appointments.filter(status=status_filter)
# Calculate statistics
total_appointments = appointments.count()
pending_appointments = appointments.filter(status='PENDING').count()
confirmed_appointments = appointments.filter(status='CONFIRMED').count()
checked_in_appointments = appointments.filter(status='CHECKED_IN').count()
in_progress_appointments = appointments.filter(status='IN_PROGRESS').count()
completed_appointments = appointments.filter(status='COMPLETED').count()
cancelled_appointments = appointments.filter(status='CANCELLED').count()
no_show_appointments = appointments.filter(status='NO_SHOW').count()
# Get queue entries for this provider
queue_entries = QueueEntry.objects.filter(
queue__tenant=tenant,
assigned_provider=provider,
status__in=['WAITING', 'CALLED', 'IN_SERVICE']
).select_related('patient', 'queue').order_by('queue_position')
# Calculate time slots (8 AM to 6 PM in 30-minute intervals)
time_slots = []
start_hour = 8
end_hour = 18
for hour in range(start_hour, end_hour):
for minute in [0, 30]:
slot_time = time(hour, minute)
time_slots.append(slot_time)
context.update({
'provider': provider,
'selected_date': selected_date,
'appointments': appointments,
'queue_entries': queue_entries,
'time_slots': time_slots,
'status_filter': status_filter,
'stats': {
'total': total_appointments,
'pending': pending_appointments,
'confirmed': confirmed_appointments,
'checked_in': checked_in_appointments,
'in_progress': in_progress_appointments,
'completed': completed_appointments,
'cancelled': cancelled_appointments,
'no_show': no_show_appointments,
},
'today': timezone.now().date(),
'yesterday': timezone.now().date() - timedelta(days=1),
'tomorrow': timezone.now().date() + timedelta(days=1),
})
return context
class ProviderWeeklyScheduleView(LoginRequiredMixin, TemplateView):
"""
Provider-facing view showing weekly schedule with appointments.
Shows appointments for the logged-in provider for a week.
"""
template_name = 'appointments/provider/provider_weekly_schedule.html'
def dispatch(self, request, *args, **kwargs):
# Ensure user is a provider (has employee profile with provider role)
if not hasattr(request.user, 'employee_profile'):
messages.error(request, 'You must be a provider to access this page.')
return redirect('core:dashboard')
# Check if user has provider role
provider_roles = ['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT',
'PHARMACIST', 'RADIOLOGIST', 'THERAPIST']
if request.user.employee_profile.role not in provider_roles:
messages.error(request, 'You must be a provider to access this page.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
provider = self.request.user
# Get selected week start date from query params or use current week
week_start_str = self.request.GET.get('week_start')
if week_start_str:
try:
week_start = datetime.strptime(week_start_str, '%Y-%m-%d').date()
except ValueError:
week_start = timezone.now().date()
else:
week_start = timezone.now().date()
# Adjust to start of week (Monday)
week_start = week_start - timedelta(days=week_start.weekday())
# Calculate week end (Sunday)
week_end = week_start + timedelta(days=6)
# Get all appointments for the week
appointments = AppointmentRequest.objects.filter(
tenant=tenant,
provider=provider,
scheduled_datetime__date__gte=week_start,
scheduled_datetime__date__lte=week_end
).select_related('patient').order_by('scheduled_datetime')
# Organize appointments by day
days_data = []
for day_offset in range(7):
current_day = week_start + timedelta(days=day_offset)
day_appointments = appointments.filter(scheduled_datetime__date=current_day)
# Calculate day statistics
day_stats = {
'total': day_appointments.count(),
'pending': day_appointments.filter(status='PENDING').count(),
'confirmed': day_appointments.filter(status='CONFIRMED').count(),
'checked_in': day_appointments.filter(status='CHECKED_IN').count(),
'in_progress': day_appointments.filter(status='IN_PROGRESS').count(),
'completed': day_appointments.filter(status='COMPLETED').count(),
'cancelled': day_appointments.filter(status='CANCELLED').count(),
}
days_data.append({
'date': current_day,
'appointments': day_appointments,
'stats': day_stats,
'is_today': current_day == timezone.now().date(),
})
# Calculate week statistics
week_stats = {
'total': appointments.count(),
'pending': appointments.filter(status='PENDING').count(),
'confirmed': appointments.filter(status='CONFIRMED').count(),
'checked_in': appointments.filter(status='CHECKED_IN').count(),
'in_progress': appointments.filter(status='IN_PROGRESS').count(),
'completed': appointments.filter(status='COMPLETED').count(),
'cancelled': appointments.filter(status='CANCELLED').count(),
'no_show': appointments.filter(status='NO_SHOW').count(),
}
context.update({
'provider': provider,
'week_start': week_start,
'week_end': week_end,
'days_data': days_data,
'week_stats': week_stats,
'prev_week': week_start - timedelta(days=7),
'next_week': week_start + timedelta(days=7),
'current_week_start': timezone.now().date() - timedelta(days=timezone.now().date().weekday()),
})
return context
class QuickCheckInView(LoginRequiredMixin, TemplateView):
"""
Provider-facing quick check-in interface.
Fast patient check-in with search and one-click functionality.
"""
template_name = 'appointments/provider/quick_check_in.html'
def dispatch(self, request, *args, **kwargs):
# Ensure user is a provider or staff member
if not hasattr(request.user, 'employee_profile'):
messages.error(request, 'You must be a staff member to access this page.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
today = timezone.now().date()
# Get search query
search_query = self.request.GET.get('search', '')
# Get today's appointments
appointments = AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status__in=['SCHEDULED', 'CONFIRMED']
).select_related('patient', 'provider').order_by('scheduled_datetime')
# Apply search filter if provided
if search_query:
appointments = appointments.filter(
Q(patient__first_name__icontains=search_query) |
Q(patient__last_name__icontains=search_query) |
Q(patient__patient_id__icontains=search_query) |
Q(patient__phone__icontains=search_query)
)
# Get recently checked-in patients (last 10)
recent_checkins = AppointmentRequest.objects.filter(
tenant=tenant,
status='CHECKED_IN',
checked_in_at__date=today
).select_related('patient', 'provider').order_by('-checked_in_at')[:10]
# Calculate statistics
stats = {
'total_today': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today
).count(),
'pending_checkin': appointments.count(),
'checked_in': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='CHECKED_IN'
).count(),
'in_progress': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='IN_PROGRESS'
).count(),
}
context.update({
'appointments': appointments,
'recent_checkins': recent_checkins,
'search_query': search_query,
'stats': stats,
'today': today,
})
return context
class ProviderQueueDashboardView(LoginRequiredMixin, TemplateView):
"""
Provider-facing simplified queue management dashboard.
Shows current patient being served and next patients in queue.
"""
template_name = 'appointments/provider/provider_queue_dashboard.html'
def dispatch(self, request, *args, **kwargs):
# Ensure user is a provider or staff member
if not hasattr(request.user, 'employee_profile'):
messages.error(request, 'You must be a staff member to access this page.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
provider = self.request.user
# Get queue_id from query params or use default
queue_id = self.request.GET.get('queue_id')
if queue_id:
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant, is_active=True)
else:
# Get first active queue for this provider or general queue
queue = WaitingQueue.objects.filter(
tenant=tenant,
is_active=True
).filter(
Q(providers=provider) | Q(providers__isnull=True)
).first()
if queue:
# Get current patient being served
current_patient = QueueEntry.objects.filter(
queue=queue,
status='IN_SERVICE'
).select_related('patient', 'appointment').first()
# Get next patients in queue (next 10)
waiting_patients = QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).select_related('patient', 'appointment').order_by('queue_position')[:10]
# Get recently completed patients (last 5)
completed_patients = QueueEntry.objects.filter(
queue=queue,
status='COMPLETED',
served_at__date=timezone.now().date()
).select_related('patient', 'appointment').order_by('-served_at')[:5]
# Calculate statistics
today = timezone.now().date()
stats = {
'total_waiting': QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).count(),
'in_service': QueueEntry.objects.filter(
queue=queue,
status='IN_SERVICE'
).count(),
'completed_today': QueueEntry.objects.filter(
queue=queue,
status='COMPLETED',
served_at__date=today
).count(),
'avg_wait_time': queue.average_service_time_minutes or 15,
}
context.update({
'queue': queue,
'current_patient': current_patient,
'waiting_patients': waiting_patients,
'completed_patients': completed_patients,
'stats': stats,
})
else:
context.update({
'queue': None,
'current_patient': None,
'waiting_patients': [],
'completed_patients': [],
'stats': {},
})
# Get all active queues for queue selector
all_queues = WaitingQueue.objects.filter(
tenant=tenant,
is_active=True
).order_by('name')
context['all_queues'] = all_queues
return context
class PatientAppointmentRequestView(LoginRequiredMixin, CreateView):
"""
Patient-facing view for requesting appointments.
Patients can only request appointments, not directly book them.
"""
model = AppointmentRequest
form_class = AppointmentRequestForm
template_name = 'appointments/patient/patient_appointment_request.html'
def dispatch(self, request, *args, **kwargs):
# Ensure user has a patient profile
if not hasattr(request.user, 'patient_profile'):
messages.error(request, 'You must have a patient profile to request appointments.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['patient'] = self.request.user.patient_profile
return context
def form_valid(self, form):
# Set patient and tenant
form.instance.patient = self.request.user.patient_profile
form.instance.tenant = self.request.user.tenant
form.instance.status = 'PENDING' # Always pending for patient requests
# Generate request ID
import uuid
form.instance.request_id = f"APT-{uuid.uuid4().hex[:8].upper()}"
response = super().form_valid(form)
# Send confirmation email
from appointments.notifications import AppointmentNotifications
AppointmentNotifications.send_appointment_confirmation(self.object)
# Log appointment request
AuditLogger.log_event(
tenant=form.instance.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Patient Appointment Request',
description=f'Patient {self.object.patient.get_full_name()} requested appointment',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(
self.request,
'Your appointment request has been submitted successfully. '
'A confirmation email has been sent to your registered email address.'
)
return response
def get_success_url(self):
return reverse('appointments:patient_appointment_success', kwargs={'pk': self.object.pk})
class PatientAppointmentListView(LoginRequiredMixin, ListView):
"""
Patient-facing view to list their own appointments.
"""
model = AppointmentRequest
template_name = 'appointments/patient/patient_appointment_list.html'
context_object_name = 'appointments'
paginate_by = 20
def dispatch(self, request, *args, **kwargs):
# Ensure user has a patient profile
if not hasattr(request.user, 'patient_profile'):
messages.error(request, 'You must have a patient profile to view appointments.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
# Only show appointments for the logged-in patient
queryset = AppointmentRequest.objects.filter(
patient=self.request.user.patient_profile,
tenant=self.request.user.tenant
).select_related('provider').order_by('-preferred_date', '-preferred_time')
# Apply filters
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
appointment_type = self.request.GET.get('appointment_type')
if appointment_type:
queryset = queryset.filter(appointment_type=appointment_type)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(preferred_date__gte=date_from)
return queryset
class PatientAppointmentDetailView(LoginRequiredMixin, DetailView):
"""
Patient-facing view to see details of their appointment.
"""
model = AppointmentRequest
template_name = 'appointments/patient/patient_appointment_detail.html'
context_object_name = 'appointment'
def dispatch(self, request, *args, **kwargs):
# Ensure user has a patient profile
if not hasattr(request.user, 'patient_profile'):
messages.error(request, 'You must have a patient profile to view appointments.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
# Only allow patients to view their own appointments
return AppointmentRequest.objects.filter(
patient=self.request.user.patient_profile,
tenant=self.request.user.tenant
).select_related('provider')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
appointment = self.get_object()
# Get telemedicine session if exists
if appointment.is_telemedicine:
context['telemedicine_session'] = TelemedicineSession.objects.filter(
appointment=appointment
).first()
return context
class PatientAppointmentCancelView(LoginRequiredMixin, UpdateView):
"""
Patient-facing view to cancel their appointment.
"""
model = AppointmentRequest
template_name = 'appointments/patient/patient_appointment_cancel.html'
context_object_name = 'appointment'
fields = [] # No fields to edit, just confirmation
def dispatch(self, request, *args, **kwargs):
# Ensure user has a patient profile
if not hasattr(request.user, 'patient_profile'):
messages.error(request, 'You must have a patient profile to cancel appointments.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
# Only allow patients to cancel their own appointments
return AppointmentRequest.objects.filter(
patient=self.request.user.patient_profile,
tenant=self.request.user.tenant,
status__in=['PENDING', 'SCHEDULED', 'CONFIRMED'] # Only cancellable statuses
)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
# Get cancellation details from form
cancellation_reason = request.POST.get('cancellation_reason', 'PATIENT_CANCELLED')
cancellation_notes = request.POST.get('cancellation_notes', '')
# Update appointment status
self.object.status = 'CANCELLED'
self.object.cancelled_at = timezone.now()
self.object.cancelled_by = request.user
self.object.cancellation_reason = cancellation_reason
self.object.cancellation_notes = cancellation_notes
self.object.save()
# Log cancellation
AuditLogger.log_event(
tenant=self.object.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Patient Cancelled Appointment',
description=f'Patient {self.object.patient.get_full_name()} cancelled appointment',
user=request.user,
content_object=self.object,
request=request
)
messages.success(
request,
'Your appointment has been cancelled successfully. '
'You can request a new appointment at any time.'
)
return redirect('appointments:patient_appointment_list')
class PatientAppointmentSuccessView(LoginRequiredMixin, DetailView):
"""
Success page shown after patient submits appointment request.
"""
model = AppointmentRequest
template_name = 'appointments/patient/patient_appointment_success.html'
context_object_name = 'appointment'
def dispatch(self, request, *args, **kwargs):
# Ensure user has a patient profile
if not hasattr(request.user, 'patient_profile'):
messages.error(request, 'You must have a patient profile.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
# Only allow patients to view their own appointments
return AppointmentRequest.objects.filter(
patient=self.request.user.patient_profile,
tenant=self.request.user.tenant
).select_related('provider')
class PatientQueueStatusView(LoginRequiredMixin, TemplateView):
"""
Patient-facing view to check their queue status.
Shows current position, estimated wait time, and queue information.
"""
template_name = 'appointments/patient/patient_queue_status.html'
def dispatch(self, request, *args, **kwargs):
# Ensure user has a patient profile
if not hasattr(request.user, 'patient_profile'):
messages.error(request, 'You must have a patient profile to view queue status.')
return redirect('core:dashboard')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
patient = self.request.user.patient_profile
tenant = self.request.user.tenant
# Get the patient's active queue entry
entry = QueueEntry.objects.filter(
patient=patient,
queue__tenant=tenant,
status__in=['WAITING', 'CALLED', 'IN_SERVICE']
).select_related('queue', 'appointment', 'assigned_provider').first()
if entry:
queue = entry.queue
# Calculate total people in queue
total_in_queue = QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).count()
# Calculate people ahead
people_ahead = QueueEntry.objects.filter(
queue=queue,
status='WAITING',
queue_position__lt=entry.queue_position
).count()
# Calculate progress percentage
if total_in_queue > 0:
progress_percentage = int(((total_in_queue - people_ahead) / total_in_queue) * 100)
else:
progress_percentage = 100
# Estimate wait time
if entry.status == 'WAITING' and people_ahead > 0:
avg_service_time = queue.average_service_time_minutes or 15
estimated_wait_time = people_ahead * avg_service_time
else:
estimated_wait_time = 0
context.update({
'entry': entry,
'queue': queue,
'total_in_queue': total_in_queue,
'people_ahead': people_ahead,
'progress_percentage': progress_percentage,
'estimated_wait_time': estimated_wait_time,
})
else:
context.update({
'entry': None,
'queue': None,
})
return context