""" 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