""" 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 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 hr.models import Schedule, Employee from .models import * from .forms import * 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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 = getattr(request, "tenant", None) 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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=getattr(self.request, 'tenant', None), 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 = getattr(self.request, 'tenant', None) 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=getattr(self.request, 'tenant', None), 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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 return kwargs def form_valid(self, form): # Set tenant form.instance.tenant = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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 return kwargs def get_success_url(self): return reverse('appointments:waiting_queue_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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=getattr(self.request, 'tenant', None), 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 = getattr(self.request, 'tenant', None) 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=getattr(self.request, 'tenant', None), 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 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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=getattr(self.request, 'tenant', None), 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 = getattr(self.request, 'tenant', None) 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=getattr(self.request, 'tenant', None), 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 = getattr(self.request, 'tenant', None) 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 = getattr(self.request, 'tenant', None) 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, 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 = getattr(self.request, 'tenant', None) if not tenant: return AppointmentTemplate.objects.none() return AppointmentTemplate.objects.filter(tenant=tenant) 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 = getattr(self.request, 'tenant', None) 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/appointment_template_form.html' permission_required = 'appointments.change_appointmenttemplate' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) 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/appointment_template_confirm_delete.html' permission_required = 'appointments.delete_appointmenttemplate' success_url = reverse_lazy('appointments:appointment_template_list') def get_queryset(self): tenant = getattr(self.request, 'tenant', None) 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'] = getattr(self.request.user, 'tenant', None) return kwargs def form_valid(self, form): form.instance.tenant = getattr(self.request.user, 'tenant', None) 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) @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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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) # def available_slots(request): # """ # HTMX view for available slots. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # provider = request.GET.get('provider') # date_str = request.GET.get('date') # # if not provider or not date_str: # return render(request, 'appointments/partials/available_slots.html', {'slots': []}) # # try: # selected_date = datetime.strptime(date_str, '%Y-%m-%d').date() # except ValueError: # return render(request, 'appointments/partials/available_slots.html', {'slots': []}) # # # Get available slots for the provider on the selected date # slots = SlotAvailability.objects.filter( # provider_id=provider, # provider__tenant=tenant, # date=selected_date, # # is_available=True # ).order_by('start_time') # # return render(request, 'appointments/partials/available_slots.html', { # 'slots': slots, # 'selected_date': selected_date # }) @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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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(), }) # def reschedule_appointment(request, pk): # """ # Reschedule an appointment. # """ # tenant = request.user.tenant # appointment = get_object_or_404(AppointmentRequest,pk=pk,tenant=tenant) # providers = User.objects.filter(tenant=tenant, role__in=['DOCTOR', 'NURSE', 'SPECIALIST']).order_by('last_name', 'first_name') # # new_date = request.POST.get('new_date') # new_time = request.POST.get('new_time') # # if new_date and new_time: # appointment.scheduled_date = new_date # appointment.scheduled_time = new_time # appointment.status = 'RESCHEDULED' # appointment.save() # # messages.success(request, 'Appointment has been rescheduled') # return redirect('appointments:appointment_detail', pk=appointment.pk) # # return render(request, 'appointments/reschedule_appointment.html', { # 'appointment': appointment, # 'providers': providers # }) @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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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 = getattr(request, 'tenant', None) 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) # class AppointmentListView(LoginRequiredMixin, ListView): # """ # List view for appointments. # """ # model = AppointmentRequest # template_name = 'appointments/requests/appointment_list.html' # context_object_name = 'appointments' # paginate_by = 20 # # def get_queryset(self): # return AppointmentRequest.objects.filter( # tenant=self.request.user.tenant # ).order_by('-created_at') # # # class AppointmentDetailView(LoginRequiredMixin, DetailView): # """ # Detail view for appointments. # """ # model = AppointmentRequest # template_name = 'appointments/requests/appointment_request_detail.html' # context_object_name = 'appointment' # # def get_queryset(self): # return AppointmentRequest.objects.filter( # tenant=self.request.user.tenant # ) 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 QueueManagementView(LoginRequiredMixin, ListView): """ Queue management view for appointments. """ model = WaitingQueue template_name = 'appointments/queue_management.html' context_object_name = 'queues' def get_queryset(self): return WaitingQueue.objects.filter( tenant=self.request.user.tenant, is_active=True, ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['queue'] = AppointmentRequest.objects.filter( tenant=self.request.user.tenant, status__in=['CONFIRMED', 'CHECKED_IN'] ).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 @login_required def calendar_view(request): """Renders the calendar page""" return render(request, "appointments/calendar.html") @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}) # # from django.shortcuts import render, get_object_or_404, redirect # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.contrib.auth.decorators import login_required # from django.http import JsonResponse, HttpResponseForbidden # from django.urls import reverse, reverse_lazy # from django.utils import timezone # from django.utils.translation import gettext_lazy as _ # from django.contrib import messages # from django.db.models import Q, Count, Avg # from django.template.loader import render_to_string # from datetime import datetime, timedelta # # from .models import ( # AppointmentRequest, SlotAvailability, WaitingQueue, # QueueEntry, TelemedicineSession, AppointmentTemplate # ) # from .forms import ( # AppointmentRequestForm, SlotAvailabilityForm, WaitingQueueForm, # QueueEntryForm, TelemedicineSessionForm, AppointmentTemplateForm, # AppointmentSearchForm # ) # from patients.models import PatientProfile # from accounts.models import User # from core.utils import get_tenant_from_request # # # class TenantRequiredMixin: # """Mixin to require a tenant for views.""" # # def dispatch(self, request, *args, **kwargs): # """Check for tenant before dispatching.""" # self.tenant = get_tenant_from_request(request) # if not self.tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # return super().dispatch(request, *args, **kwargs) # # # # --- AppointmentRequest Views --- # # class AppointmentRequestListView(LoginRequiredMixin, TenantRequiredMixin, ListView): # """List view for appointment requests.""" # model = AppointmentRequest # template_name = 'appointments/appointment_list.html' # context_object_name = 'appointments' # paginate_by = 20 # # def get_queryset(self): # """Get filtered queryset based on search parameters.""" # queryset = AppointmentRequest.objects.filter(tenant=self.tenant) # # # Initialize the search form # self.form = AppointmentSearchForm(self.request.GET, tenant=self.tenant) # # # Apply filters if the form is valid # if self.form.is_valid(): # data = self.form.cleaned_data # # # Text search # if data.get('search'): # search_term = data['search'] # queryset = queryset.filter( # Q(patient__first_name__icontains=search_term) | # Q(patient__last_name__icontains=search_term) | # Q(provider__first_name__icontains=search_term) | # Q(provider__last_name__icontains=search_term) | # Q(reason__icontains=search_term) # ) # # # Status filter # if data.get('status'): # queryset = queryset.filter(status=data['status']) # # # Appointment type filter # if data.get('appointment_type'): # queryset = queryset.filter(appointment_type=data['appointment_type']) # # # Provider filter # if data.get('provider'): # queryset = queryset.filter(provider=data['provider']) # # # Department filter # if data.get('department'): # queryset = queryset.filter(department=data['department']) # # # Date range filters # if data.get('date_from'): # queryset = queryset.filter(scheduled_datetime__date__gte=data['date_from']) # # if data.get('date_to'): # queryset = queryset.filter(scheduled_datetime__date__lte=data['date_to']) # # # Default ordering # return queryset.select_related('patient', 'provider', 'department').order_by( # 'scheduled_datetime' # ) # # def get_context_data(self, **kwargs): # """Add search form and filters to context.""" # context = super().get_context_data(**kwargs) # context['form'] = self.form # # # Add filter summaries # context['active_filters'] = {} # if self.form.is_valid(): # data = self.form.cleaned_data # for field, value in data.items(): # if value: # if field == 'provider' and value: # context['active_filters'][field] = value.get_full_name() # elif field == 'department' and value: # context['active_filters'][field] = value.name # elif field in ['status', 'appointment_type'] and value: # # Get display value for choice fields # choices_dict = dict(getattr(AppointmentRequest, f"{field.upper()}_CHOICES", [])) # context['active_filters'][field] = choices_dict.get(value, value) # else: # context['active_filters'][field] = value # # # Add appointment counts by status # context['appointment_counts'] = { # 'total': AppointmentRequest.objects.filter(tenant=self.tenant).count(), # 'upcoming': AppointmentRequest.objects.filter( # tenant=self.tenant, # scheduled_datetime__gte=timezone.now(), # status__in=['SCHEDULED', 'CONFIRMED'] # ).count(), # 'today': AppointmentRequest.objects.filter( # tenant=self.tenant, # scheduled_datetime__date=timezone.now().date() # ).count(), # 'pending': AppointmentRequest.objects.filter( # tenant=self.tenant, # status='REQUESTED' # ).count(), # } # # return context # # # class AppointmentRequestDetailView(LoginRequiredMixin, TenantRequiredMixin, DetailView): # """Detail view for an appointment request.""" # model = AppointmentRequest # template_name = 'appointments/appointment_detail.html' # context_object_name = 'appointment' # # def get_queryset(self): # """Filter by tenant.""" # return AppointmentRequest.objects.filter( # tenant=self.tenant # ).select_related( # 'patient', 'provider', 'department', 'telemedicine_session', # 'template', 'requested_by', 'cancelled_by' # ) # # def get_context_data(self, **kwargs): # """Add additional context data.""" # context = super().get_context_data(**kwargs) # # # Add related queue entries # context['queue_entries'] = self.object.queue_entries.all() # # # Check if patient has other appointments # context['other_appointments'] = AppointmentRequest.objects.filter( # tenant=self.tenant, # patient=self.object.patient # ).exclude( # pk=self.object.pk # ).order_by('-scheduled_datetime')[:5] # # return context # # # class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, CreateView): # """Create view for a new appointment request.""" # model = AppointmentRequest # form_class = AppointmentRequestForm # template_name = 'appointments/appointment_form.html' # permission_required = 'appointments.add_appointmentrequest' # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_initial(self): # """Pre-populate form with initial values.""" # initial = super().get_initial() # # # Pre-populate patient if provided in query params # patient_id = self.request.GET.get('patient_id') # if patient_id: # try: # patient = PatientProfile.objects.get( # tenant=self.tenant, # patient_id=patient_id # ) # initial['patient'] = patient # except PatientProfile.DoesNotExist: # pass # # # Pre-populate provider if provided in query params # provider_id = self.request.GET.get('provider_id') # if provider_id: # try: # provider = User.objects.get( # tenant=self.tenant, # id=provider_id # ) # initial['provider'] = provider # except User.DoesNotExist: # pass # # # Pre-populate date/time if provided # date_str = self.request.GET.get('date') # time_str = self.request.GET.get('time') # if date_str: # try: # if time_str: # # Combine date and time # date_time = datetime.strptime( # f"{date_str} {time_str}", # '%Y-%m-%d %H:%M' # ) # else: # # Just date, use default time (9:00 AM) # date_time = datetime.strptime( # f"{date_str} 09:00", # '%Y-%m-%d %H:%M' # ) # # # Convert to timezone-aware datetime # date_time = timezone.make_aware(date_time) # initial['scheduled_datetime'] = date_time # except ValueError: # pass # # # Set default status # initial['status'] = 'SCHEDULED' # # # Set default appointment type # initial['appointment_type'] = 'IN_PERSON' # # # Set default duration # initial['duration_minutes'] = 30 # # return initial # # def form_valid(self, form): # """Process the valid form.""" # # Set tenant # form.instance.tenant = self.tenant # # # Set requested_by to current user if not set # if not form.instance.requested_by: # form.instance.requested_by = self.request.user # # # Save the form # response = super().form_valid(form) # # # Log the creation # messages.success( # self.request, # _("Appointment for {} with {} has been scheduled for {}.").format( # form.instance.patient.get_full_name(), # form.instance.provider.get_full_name(), # form.instance.scheduled_datetime.strftime('%Y-%m-%d %H:%M') # ) # ) # # return response # # def get_success_url(self): # """Redirect to appointment detail view.""" # return reverse('appointments:appointment_detail', kwargs={'pk': self.object.pk}) # # # class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, UpdateView): # """Update view for an appointment request.""" # model = AppointmentRequest # form_class = AppointmentRequestForm # template_name = 'appointments/appointment_form.html' # permission_required = 'appointments.change_appointmentrequest' # # def get_queryset(self): # """Filter by tenant.""" # return AppointmentRequest.objects.filter(tenant=self.tenant) # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Check if status changed # old_status = AppointmentRequest.objects.get(pk=self.object.pk).status # new_status = form.cleaned_data.get('status') # # # Save the form # response = super().form_valid(form) # # # Add appropriate message # if old_status != new_status: # messages.success( # self.request, # _("Appointment status updated from {} to {}.").format( # dict(AppointmentRequest.STATUS_CHOICES).get(old_status, old_status), # dict(AppointmentRequest.STATUS_CHOICES).get(new_status, new_status), # ) # ) # else: # messages.success( # self.request, # _("Appointment updated successfully.") # ) # # return response # # def get_success_url(self): # """Redirect to appointment detail view.""" # return reverse('appointments:appointment_detail', kwargs={'pk': self.object.pk}) # # # class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, DeleteView): # """Delete view for an appointment request.""" # model = AppointmentRequest # template_name = 'appointments/appointment_confirm_delete.html' # permission_required = 'appointments.delete_appointmentrequest' # success_url = reverse_lazy('appointments:appointment_list') # # def get_queryset(self): # """Filter by tenant.""" # return AppointmentRequest.objects.filter(tenant=self.tenant) # # def delete(self, request, *args, **kwargs): # """Override delete to add custom message.""" # self.object = self.get_object() # success_url = self.get_success_url() # # # Store patient name for message # patient_name = self.object.patient.get_full_name() if self.object.patient else "Unknown" # appointment_date = self.object.scheduled_datetime.strftime( # '%Y-%m-%d %H:%M') if self.object.scheduled_datetime else "Unscheduled" # # # Delete the object # self.object.delete() # # # Add success message # messages.success( # request, # _("Appointment for {} on {} has been deleted.").format( # patient_name, appointment_date # ) # ) # # return redirect(success_url) # # # # --- SlotAvailability Views --- # # class SlotAvailabilityListView(LoginRequiredMixin, TenantRequiredMixin, ListView): # """List view for provider availability slots.""" # model = SlotAvailability # template_name = 'appointments/slot_availability_list.html' # context_object_name = 'slots' # paginate_by = 20 # # def get_queryset(self): # """Filter by tenant and provider.""" # queryset = SlotAvailability.objects.filter(tenant=self.tenant) # # # Filter by provider if specified # provider_id = self.request.GET.get('provider_id') # if provider_id: # queryset = queryset.filter(provider_id=provider_id) # # # Filter by date range # date_from = self.request.GET.get('date_from') # if date_from: # try: # date_from = datetime.strptime(date_from, '%Y-%m-%d').date() # queryset = queryset.filter(start_time__date__gte=date_from) # except ValueError: # pass # # date_to = self.request.GET.get('date_to') # if date_to: # try: # date_to = datetime.strptime(date_to, '%Y-%m-%d').date() # queryset = queryset.filter(start_time__date__lte=date_to) # except ValueError: # pass # # # Default ordering # return queryset.select_related('provider').order_by('start_time') # # def get_context_data(self, **kwargs): # """Add filter context.""" # context = super().get_context_data(**kwargs) # # # Add provider filter # provider_id = self.request.GET.get('provider_id') # if provider_id: # try: # context['selected_provider'] = User.objects.get(id=provider_id) # except User.DoesNotExist: # pass # # # Add providers for filter dropdown # context['providers'] = User.objects.filter( # tenant=self.tenant, # role__in=['DOCTOR', 'NURSE', 'PHYSICIAN_ASSISTANT'] # ).order_by('first_name', 'last_name') # # # Add date filters # context['date_from'] = self.request.GET.get('date_from', '') # context['date_to'] = self.request.GET.get('date_to', '') # # return context # # # class SlotAvailabilityDetailView(LoginRequiredMixin, TenantRequiredMixin, DetailView): # """Detail view for a provider availability slot.""" # model = SlotAvailability # template_name = 'appointments/slot_availability_detail.html' # context_object_name = 'slot' # # def get_queryset(self): # """Filter by tenant.""" # return SlotAvailability.objects.filter(tenant=self.tenant).select_related('provider') # # def get_context_data(self, **kwargs): # """Add additional context data.""" # context = super().get_context_data(**kwargs) # # # Get appointments during this slot # context['appointments'] = self.object.get_appointments() # # return context # # # class SlotAvailabilityCreateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, CreateView): # """Create view for a new availability slot.""" # model = SlotAvailability # form_class = SlotAvailabilityForm # template_name = 'appointments/slot_availability_form.html' # permission_required = 'appointments.add_slotavailability' # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_initial(self): # """Pre-populate form with initial values.""" # initial = super().get_initial() # # # Pre-populate provider if provided in query params # provider_id = self.request.GET.get('provider_id') # if provider_id: # try: # provider = User.objects.get(tenant=self.tenant, id=provider_id) # initial['provider'] = provider # except User.DoesNotExist: # pass # # # Pre-populate date/time if provided # date_str = self.request.GET.get('date') # if date_str: # try: # # Parse date and set default times (9:00 AM - 5:00 PM) # date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() # start_time = timezone.make_aware(datetime.combine(date_obj, datetime.min.time().replace(hour=9))) # end_time = timezone.make_aware(datetime.combine(date_obj, datetime.min.time().replace(hour=17))) # # initial['start_time'] = start_time # initial['end_time'] = end_time # except ValueError: # pass # # # Set default availability # initial['is_available'] = True # # return initial # # def form_valid(self, form): # """Process the valid form.""" # # Set tenant # form.instance.tenant = self.tenant # # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Availability slot for {} from {} to {} has been created.").format( # form.instance.provider.get_full_name(), # form.instance.start_time.strftime('%Y-%m-%d %H:%M'), # form.instance.end_time.strftime('%H:%M') # ) # ) # # return response # # def get_success_url(self): # """Redirect to slot list view.""" # return reverse('appointments:slot_availability_list') # # # class SlotAvailabilityUpdateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, UpdateView): # """Update view for an availability slot.""" # model = SlotAvailability # form_class = SlotAvailabilityForm # template_name = 'appointments/slot_availability_form.html' # permission_required = 'appointments.change_slotavailability' # # def get_queryset(self): # """Filter by tenant.""" # return SlotAvailability.objects.filter(tenant=self.tenant) # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Availability slot updated successfully.") # ) # # return response # # def get_success_url(self): # """Redirect to slot detail view.""" # return reverse('appointments:slot_availability_detail', kwargs={'pk': self.object.pk}) # # # class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, DeleteView): # """Delete view for an availability slot.""" # model = SlotAvailability # template_name = 'appointments/slot_availability_confirm_delete.html' # permission_required = 'appointments.delete_slotavailability' # success_url = reverse_lazy('appointments:slot_availability_list') # # def get_queryset(self): # """Filter by tenant.""" # return SlotAvailability.objects.filter(tenant=self.tenant) # # def delete(self, request, *args, **kwargs): # """Override delete to add custom message.""" # self.object = self.get_object() # success_url = self.get_success_url() # # # Store information for message # provider_name = self.object.provider.get_full_name() if self.object.provider else "Unknown" # slot_date = self.object.start_time.strftime('%Y-%m-%d') # # # Delete the object # self.object.delete() # # # Add success message # messages.success( # request, # _("Availability slot for {} on {} has been deleted.").format( # provider_name, slot_date # ) # ) # # return redirect(success_url) # # # # --- WaitingQueue Views --- # # class WaitingQueueListView(LoginRequiredMixin, TenantRequiredMixin, ListView): # """List view for waiting queues.""" # model = WaitingQueue # template_name = 'appointments/waiting_queue_list.html' # context_object_name = 'queues' # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter( # tenant=self.tenant # ).annotate( # waiting_count=Count('entries', filter=Q(entries__status='WAITING')) # ).select_related('department', 'provider').order_by('name') # # def get_context_data(self, **kwargs): # """Add department filter context.""" # context = super().get_context_data(**kwargs) # # # Add department filter # department_id = self.request.GET.get('department_id') # if department_id: # context['queues'] = context['queues'].filter(department_id=department_id) # try: # context['selected_department'] = department_id # except: # pass # # return context # # # class WaitingQueueDetailView(LoginRequiredMixin, TenantRequiredMixin, DetailView): # """Detail view for a waiting queue.""" # model = WaitingQueue # template_name = 'appointments/waiting_queue_detail.html' # context_object_name = 'queue' # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter(tenant=self.tenant).select_related('department', 'provider') # # def get_context_data(self, **kwargs): # """Add entries to context.""" # context = super().get_context_data(**kwargs) # # # Get waiting entries # context['waiting_entries'] = self.object.entries.filter( # status='WAITING' # ).select_related('patient').order_by('priority', 'added_time') # # # Get called/in-progress entries # context['active_entries'] = self.object.entries.filter( # status__in=['CALLED', 'IN_PROGRESS'] # ).select_related('patient').order_by('called_time') # # # Get completed entries from today # today = timezone.now().date() # context['completed_entries'] = self.object.entries.filter( # status='COMPLETED', # completed_time__date=today # ).select_related('patient').order_by('-completed_time') # # # Get statistics # context['stats'] = { # 'avg_wait_time': self.object.entries.filter( # status__in=['COMPLETED', 'CALLED', 'IN_PROGRESS'], # wait_time__isnull=False # ).aggregate(avg=Avg('wait_time'))['avg'] or 0, # # 'today_count': self.object.entries.filter( # added_time__date=today # ).count(), # # 'completed_count': self.object.entries.filter( # status='COMPLETED', # completed_time__date=today # ).count(), # } # # # Add form for adding a new entry # context['entry_form'] = QueueEntryForm( # user=self.request.user, # initial={'queue': self.object, 'priority': 'ROUTINE'} # ) # # return context # # # class WaitingQueueCreateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, CreateView): # """Create view for a new waiting queue.""" # model = WaitingQueue # form_class = WaitingQueueForm # template_name = 'appointments/waiting_queue_form.html' # permission_required = 'appointments.add_waitingqueue' # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Set tenant # form.instance.tenant = self.tenant # # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Waiting queue '{}' has been created.").format( # form.instance.name # ) # ) # # return response # # def get_success_url(self): # """Redirect to queue detail view.""" # return reverse('appointments:waiting_queue_detail', kwargs={'pk': self.object.pk}) # # # class WaitingQueueUpdateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, UpdateView): # """Update view for a waiting queue.""" # model = WaitingQueue # form_class = WaitingQueueForm # template_name = 'appointments/waiting_queue_form.html' # permission_required = 'appointments.change_waitingqueue' # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter(tenant=self.tenant) # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Waiting queue '{}' has been updated.").format( # form.instance.name # ) # ) # # return response # # def get_success_url(self): # """Redirect to queue detail view.""" # return reverse('appointments:waiting_queue_detail', kwargs={'pk': self.object.pk}) # # # class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, DeleteView): # """Delete view for a waiting queue.""" # model = WaitingQueue # template_name = 'appointments/waiting_queue_confirm_delete.html' # permission_required = 'appointments.delete_waitingqueue' # success_url = reverse_lazy('appointments:waiting_queue_list') # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter(tenant=self.tenant) # # def delete(self, request, *args, **kwargs): # """Override delete to add custom message.""" # self.object = self.get_object() # success_url = self.get_success_url() # # # Store information for message # queue_name = self.object.name # # # Check if there are active entries # active_entries = self.object.entries.filter( # status__in=['WAITING', 'CALLED', 'IN_PROGRESS'] # ).exists() # # if active_entries: # messages.error( # request, # _("Cannot delete queue '{}' because it has active entries.").format(queue_name) # ) # return redirect('appointments:waiting_queue_detail', pk=self.object.pk) # # # Delete the object # self.object.delete() # # # Add success message # messages.success( # request, # _("Waiting queue '{}' has been deleted.").format(queue_name) # ) # # return redirect(success_url) # # # # --- Calendar Views --- # # @login_required # def calendar_view(request): # """View for the appointment calendar.""" # tenant = get_tenant_from_request(request) # if not tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # # # Get providers for filter # providers = User.objects.filter( # tenant=tenant, # role__in=['DOCTOR', 'NURSE', 'PHYSICIAN_ASSISTANT'] # ).order_by('first_name', 'last_name') # # # Get selected date (default to today) # date_str = request.GET.get('date') # 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 selected provider (optional) # provider_id = request.GET.get('provider_id') # selected_provider = None # if provider_id: # try: # selected_provider = User.objects.get(id=provider_id, tenant=tenant) # except User.DoesNotExist: # pass # # # Calendar navigation # prev_date = selected_date - timedelta(days=1) # next_date = selected_date + timedelta(days=1) # # # Week navigation # start_of_week = selected_date - timedelta(days=selected_date.weekday()) # week_dates = [start_of_week + timedelta(days=i) for i in range(7)] # # context = { # 'providers': providers, # 'selected_date': selected_date, # 'selected_provider': selected_provider, # 'prev_date': prev_date, # 'next_date': next_date, # 'week_dates': week_dates, # } # # return render(request, 'appointments/calendar.html', context) # # # @login_required # def calendar_appointments(request): # """ # HTMX view for calendar appointments. # """ # tenant = get_tenant_from_request(request) # 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.select_related('patient', 'provider').order_by('scheduled_datetime') # # # Get time slots (30-minute intervals from 8:00 to 18:00) # start_hour = 8 # end_hour = 18 # time_slots = [] # # for hour in range(start_hour, end_hour): # for minute in [0, 30]: # time_slots.append({ # 'time': f"{hour:02d}:{minute:02d}", # 'datetime': datetime.combine(selected_date, datetime.min.time().replace(hour=hour, minute=minute)) # }) # # # Organize appointments by time slot # for slot in time_slots: # slot_time = timezone.make_aware(slot['datetime']) # slot_end = slot_time + timedelta(minutes=30) # # # Find appointments that overlap with this slot # slot['appointments'] = [ # appt for appt in appointments if ( # appt.scheduled_datetime < slot_end and # appt.scheduled_datetime + timedelta(minutes=appt.duration_minutes) > slot_time # ) # ] # # return render(request, 'appointments/partials/calendar_appointments.html', { # 'appointments': appointments, # 'selected_date': selected_date, # 'time_slots': time_slots # }) # # # @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 # }) # # # # --- HTMX Queue Management Views --- # # @login_required # def queue_status(request, queue_id): # """ # HTMX view for queue status updates. # """ # 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) # # # Get waiting entries # waiting_entries = queue.entries.filter( # status='WAITING' # ).select_related('patient').order_by('priority', 'added_time') # # # Get called/in-progress entries # active_entries = queue.entries.filter( # status__in=['CALLED', 'IN_PROGRESS'] # ).select_related('patient').order_by('called_time') # # return render(request, 'appointments/partials/queue_status.html', { # 'queue': queue, # 'waiting_entries': waiting_entries, # 'active_entries': active_entries # }) # # # @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) # # # # --- Action Views --- # # @login_required # def cancel_appointment(request, pk): # """ # View for cancelling 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 cancel appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # if request.method == 'POST': # reason = request.POST.get('reason', '') # # # Cancel the appointment # appointment.cancel(request.user, reason) # # messages.success(request, _("Appointment has been cancelled.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # return render(request, 'appointments/cancel_appointment.html', { # 'appointment': appointment # }) # # # @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 start_appointment(request, pk): # """ # View for starting 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 start appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Check if the appointment can be started # if appointment.status != 'CHECKED_IN': # messages.error(request, _("This appointment cannot be started.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Start the appointment # appointment.start_appointment() # # messages.success(request, _("Appointment has been started.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # @login_required # def complete_appointment(request, pk): # """ # View for completing 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 complete appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Check if the appointment can be completed # if appointment.status != 'IN_PROGRESS': # messages.error(request, _("This appointment cannot be marked as completed.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Complete the appointment # appointment.complete_appointment() # # messages.success(request, _("Appointment has been completed.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # @login_required # def reschedule_appointment(request, pk): # """ # View for rescheduling 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 reschedule appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Check if the appointment can be rescheduled # if appointment.status in ['COMPLETED', 'CANCELLED', 'NO_SHOW']: # messages.error(request, _("This appointment cannot be rescheduled.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # if request.method == 'POST': # form = AppointmentRequestForm(request.POST, instance=appointment, user=request.user) # if form.is_valid(): # # Update the appointment # appointment = form.save() # # # Set status to RESCHEDULED if it was already scheduled # if appointment.status in ['SCHEDULED', 'CONFIRMED']: # appointment.status = 'RESCHEDULED' # appointment.save() # # messages.success(request, _("Appointment has been rescheduled.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # else: # form = AppointmentRequestForm(instance=appointment, user=request.user) # # return render(request, 'appointments/appointment_reschedule.html', { # 'form': form, # 'appointment': appointment # }) # # # # --- HTMX Views for Telemedicine --- # # @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 # }) # # from django.shortcuts import render, redirect, get_object_or_404 # from django.contrib.auth.decorators import login_required, permission_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.contrib import messages # from django.views.generic import ( # CreateView, UpdateView, DeleteView, DetailView, ListView, FormView # ) # from django.urls import reverse_lazy, reverse # from django.http import JsonResponse, HttpResponse # from django.utils import timezone # from django.db import transaction # from django.core.mail import send_mail # from django.conf import settings # from django.db.models import Q, Count # from viewflow.views import CreateProcessView, UpdateProcessView # from datetime import datetime, timedelta # import json # # from .models import Appointment, AppointmentConfirmation, Queue, TelemedicineSession # from .forms import ( # AppointmentSchedulingForm, AppointmentConfirmationForm, QueueManagementForm, # TelemedicineSetupForm, AppointmentRescheduleForm, AppointmentCancellationForm, # AppointmentCheckInForm, BulkAppointmentForm # ) # from .flows import AppointmentSchedulingFlow, AppointmentConfirmationFlow, QueueManagementFlow, TelemedicineSetupFlow # from patients.models import Patient # # # class AppointmentSchedulingView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for appointment scheduling workflow # """ # model = Appointment # form_class = AppointmentSchedulingForm # template_name = 'appointments/appointment_scheduling.html' # permission_required = 'appointments.can_schedule_appointments' # flow_class = AppointmentSchedulingFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # return kwargs # # def form_valid(self, form): # with transaction.atomic(): # # Create appointment # appointment = form.save(commit=False) # appointment.tenant = self.request.user.tenant # appointment.scheduled_by = self.request.user # appointment.status = 'scheduled' # appointment.save() # # # Start appointment scheduling workflow # process = self.flow_class.start.run( # appointment=appointment, # send_confirmation=form.cleaned_data.get('send_confirmation', True), # send_reminder=form.cleaned_data.get('send_reminder', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Appointment scheduled successfully for {appointment.patient.get_full_name()} ' # f'on {appointment.appointment_date} at {appointment.appointment_time}.' # ) # # return redirect('appointments:appointment_detail', pk=appointment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Schedule Appointment' # context['breadcrumbs'] = [ # {'name': 'Home', 'url': reverse('core:dashboard')}, # {'name': 'Appointments', 'url': reverse('appointments:appointment_list')}, # {'name': 'Schedule Appointment', 'url': ''} # ] # context['available_slots'] = self.get_available_slots() # return context # # def get_available_slots(self): # """Get available appointment slots for the next 7 days""" # slots = [] # today = timezone.now().date() # # for i in range(7): # date = today + timedelta(days=i) # day_slots = self.get_slots_for_date(date) # if day_slots: # slots.append({ # 'date': date, # 'slots': day_slots # }) # # return slots # # def get_slots_for_date(self, date): # """Get available slots for a specific date""" # # This would implement slot availability logic # return [] # # # class AppointmentConfirmationView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for appointment confirmation workflow # """ # model = AppointmentConfirmation # form_class = AppointmentConfirmationForm # template_name = 'appointments/appointment_confirmation.html' # permission_required = 'appointments.can_confirm_appointments' # flow_class = AppointmentConfirmationFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # return kwargs # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # # with transaction.atomic(): # # Create confirmation record # confirmation = form.save(commit=False) # confirmation.appointment = appointment # confirmation.confirmed_by = self.request.user # confirmation.confirmed_at = timezone.now() # confirmation.save() # # # Update appointment status # if form.cleaned_data.get('reschedule_requested'): # appointment.status = 'reschedule_requested' # appointment.save() # # # Start rescheduling process # messages.info( # self.request, # 'Patient requested reschedule. Please process the reschedule request.' # ) # return redirect('appointments:appointment_reschedule', pk=appointment.pk) # else: # appointment.status = 'confirmed' # appointment.save() # # # Start confirmation workflow # process = self.flow_class.start.run( # appointment=appointment, # confirmation=confirmation, # created_by=self.request.user # ) # # messages.success( # self.request, # f'Appointment confirmed for {appointment.patient.get_full_name()}.' # ) # # return redirect('appointments:appointment_detail', pk=appointment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # context['title'] = 'Confirm Appointment' # return context # # # class QueueManagementView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for queue management workflow # """ # model = Queue # form_class = QueueManagementForm # template_name = 'appointments/queue_management.html' # permission_required = 'appointments.can_manage_queue' # flow_class = QueueManagementFlow # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # # with transaction.atomic(): # # Create or update queue entry # queue_entry, created = Queue.objects.get_or_create( # appointment=appointment, # defaults={ # 'tenant': self.request.user.tenant, # 'patient': appointment.patient, # 'provider': appointment.provider, # 'department': appointment.department, # 'created_by': self.request.user # } # ) # # # Update queue entry with form data # for field in form.cleaned_data: # if hasattr(queue_entry, field): # setattr(queue_entry, field, form.cleaned_data[field]) # # queue_entry.save() # # # Start queue management workflow # process = self.flow_class.start.run( # queue_entry=queue_entry, # appointment=appointment, # notify_patient=form.cleaned_data.get('notify_patient', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Queue updated for {appointment.patient.get_full_name()}. ' # f'Position: {queue_entry.position}, Wait time: {queue_entry.estimated_wait_time} minutes.' # ) # # return redirect('appointments:queue_list') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # context['title'] = 'Manage Queue' # context['current_queue'] = self.get_current_queue() # return context # # def get_current_queue(self): # """Get current queue status""" # return Queue.objects.filter( # tenant=self.request.user.tenant, # status='waiting' # ).order_by('priority', 'created_at') # # # class TelemedicineSetupView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for telemedicine setup workflow # """ # model = TelemedicineSession # form_class = TelemedicineSetupForm # template_name = 'appointments/telemedicine_setup.html' # permission_required = 'appointments.can_setup_telemedicine' # flow_class = TelemedicineSetupFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # return kwargs # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # # with transaction.atomic(): # # Create telemedicine session # session = form.save(commit=False) # session.appointment = appointment # session.tenant = self.request.user.tenant # session.created_by = self.request.user # session.save() # # # Update appointment type # appointment.appointment_type = 'telemedicine' # appointment.save() # # # Start telemedicine setup workflow # process = self.flow_class.start.run( # session=session, # appointment=appointment, # test_connection=form.cleaned_data.get('test_connection', True), # send_instructions=form.cleaned_data.get('send_instructions', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Telemedicine session setup completed for {appointment.patient.get_full_name()}. ' # f'Meeting details have been sent to the patient.' # ) # # return redirect('appointments:appointment_detail', pk=appointment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # context['title'] = 'Setup Telemedicine' # return context # # # class AppointmentRescheduleView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for appointment rescheduling # """ # form_class = AppointmentRescheduleForm # template_name = 'appointments/appointment_reschedule.html' # permission_required = 'appointments.can_reschedule_appointments' # # def get_success_url(self): # return reverse('appointments:appointment_detail', kwargs={'pk': self.kwargs['pk']}) # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # return kwargs # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['pk']) # # with transaction.atomic(): # # Store original appointment details # original_date = appointment.appointment_date # original_time = appointment.appointment_time # original_provider = appointment.provider # # # Update appointment # appointment.appointment_date = form.cleaned_data['new_date'] # appointment.appointment_time = form.cleaned_data['new_time'] # if form.cleaned_data.get('new_provider'): # appointment.provider = form.cleaned_data['new_provider'] # appointment.status = 'rescheduled' # appointment.save() # # # Log reschedule # self.log_reschedule(appointment, original_date, original_time, original_provider, form.cleaned_data) # # # Send notifications # if form.cleaned_data.get('notify_patient'): # self.send_reschedule_notification(appointment, form.cleaned_data) # # messages.success( # self.request, # f'Appointment rescheduled successfully. New date: {appointment.appointment_date}, ' # f'New time: {appointment.appointment_time}.' # ) # # return super().form_valid(form) # # def log_reschedule(self, appointment, original_date, original_time, original_provider, form_data): # """Log reschedule details""" # from core.models import AuditLogEntry # AuditLogEntry.objects.create( # tenant=appointment.tenant, # user=self.request.user, # event_type='APPOINTMENT_RESCHEDULE', # action='UPDATE', # object_type='Appointment', # object_id=str(appointment.id), # details={ # 'original_date': original_date.isoformat(), # 'original_time': original_time.isoformat(), # 'original_provider': original_provider.get_full_name() if original_provider else None, # 'new_date': form_data['new_date'].isoformat(), # 'new_time': form_data['new_time'].isoformat(), # 'new_provider': form_data['new_provider'].get_full_name() if form_data.get('new_provider') else None, # 'reason': form_data['reschedule_reason'], # 'notes': form_data.get('notes', '') # }, # ip_address=self.request.META.get('REMOTE_ADDR'), # user_agent=self.request.META.get('HTTP_USER_AGENT', '') # ) # # def send_reschedule_notification(self, appointment, form_data): # """Send reschedule notification to patient""" # if appointment.patient.email: # send_mail( # subject='Appointment Rescheduled', # message=f'Your appointment has been rescheduled to {appointment.appointment_date} at {appointment.appointment_time}.', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # context['title'] = 'Reschedule Appointment' # return context # # # class AppointmentCancellationView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for appointment cancellation # """ # form_class = AppointmentCancellationForm # template_name = 'appointments/appointment_cancellation.html' # permission_required = 'appointments.can_cancel_appointments' # # def get_success_url(self): # return reverse('appointments:appointment_list') # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['pk']) # # with transaction.atomic(): # # Update appointment status # appointment.status = 'cancelled' # appointment.cancelled_at = timezone.now() # appointment.cancelled_by = self.request.user # appointment.cancellation_reason = form.cleaned_data['cancellation_reason'] # appointment.cancellation_notes = form.cleaned_data.get('cancellation_notes', '') # appointment.save() # # # Handle follow-up actions # if form.cleaned_data.get('offer_reschedule'): # self.offer_reschedule(appointment) # # if form.cleaned_data.get('notify_patient'): # self.send_cancellation_notification(appointment, form.cleaned_data) # # if form.cleaned_data.get('refund_required'): # self.process_refund(appointment) # # messages.success( # self.request, # f'Appointment cancelled for {appointment.patient.get_full_name()}.' # ) # # return super().form_valid(form) # # def offer_reschedule(self, appointment): # """Offer reschedule options to patient""" # # This would implement reschedule offering logic # pass # # def send_cancellation_notification(self, appointment, form_data): # """Send cancellation notification to patient""" # if appointment.patient.email: # send_mail( # subject='Appointment Cancelled', # message=f'Your appointment on {appointment.appointment_date} has been cancelled. Reason: {form_data["cancellation_reason"]}', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def process_refund(self, appointment): # """Process refund if required""" # # This would implement refund processing logic # pass # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # context['title'] = 'Cancel Appointment' # return context # # # class AppointmentCheckInView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for appointment check-in # """ # form_class = AppointmentCheckInForm # template_name = 'appointments/appointment_checkin.html' # permission_required = 'appointments.can_checkin_patients' # # def get_success_url(self): # return reverse('appointments:appointment_detail', kwargs={'pk': self.kwargs['pk']}) # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['pk']) # # with transaction.atomic(): # # Update appointment status # appointment.status = 'checked_in' # appointment.checked_in_at = timezone.now() # appointment.checked_in_by = self.request.user # appointment.arrival_time = form.cleaned_data['arrival_time'] # appointment.save() # # # Create queue entry if needed # if not hasattr(appointment, 'queue_entry'): # Queue.objects.create( # appointment=appointment, # tenant=appointment.tenant, # patient=appointment.patient, # provider=appointment.provider, # department=appointment.department, # queue_type='check_in', # priority='normal', # status='waiting', # created_by=self.request.user # ) # # # Handle pre-visit tasks # self.process_checkin_tasks(appointment, form.cleaned_data) # # messages.success( # self.request, # f'{appointment.patient.get_full_name()} checked in successfully.' # ) # # return super().form_valid(form) # # def process_checkin_tasks(self, appointment, form_data): # """Process check-in tasks""" # tasks = [] # # if form_data.get('insurance_verified'): # tasks.append('Insurance verified') # if form_data.get('copay_collected'): # tasks.append('Copay collected') # if form_data.get('forms_completed'): # tasks.append('Forms completed') # if form_data.get('vitals_required'): # tasks.append('Vitals required') # # # Log completed tasks # if tasks: # from core.models import AuditLogEntry # AuditLogEntry.objects.create( # tenant=appointment.tenant, # user=self.request.user, # event_type='APPOINTMENT_CHECKIN', # action='UPDATE', # object_type='Appointment', # object_id=str(appointment.id), # details={ # 'completed_tasks': tasks, # 'special_needs': form_data.get('special_needs', '') # }, # ip_address=self.request.META.get('REMOTE_ADDR'), # user_agent=self.request.META.get('HTTP_USER_AGENT', '') # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # context['title'] = 'Check In Patient' # return context # # # class BulkAppointmentView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for bulk appointment operations # """ # form_class = BulkAppointmentForm # template_name = 'appointments/bulk_appointment.html' # permission_required = 'appointments.can_bulk_manage_appointments' # # def get_success_url(self): # return reverse('appointments:appointment_list') # # def form_valid(self, form): # appointment_ids = form.cleaned_data['appointment_ids'].split(',') # appointments = Appointment.objects.filter( # id__in=appointment_ids, # tenant=self.request.user.tenant # ) # # action = form.cleaned_data['action'] # # with transaction.atomic(): # if action == 'confirm': # self.bulk_confirm(appointments, form.cleaned_data) # elif action == 'reschedule': # self.bulk_reschedule(appointments, form.cleaned_data) # elif action == 'cancel': # self.bulk_cancel(appointments, form.cleaned_data) # elif action == 'send_reminders': # self.bulk_send_reminders(appointments, form.cleaned_data) # # messages.success( # self.request, # f'Bulk operation "{action}" completed for {appointments.count()} appointments.' # ) # # return super().form_valid(form) # # def bulk_confirm(self, appointments, form_data): # """Bulk confirm appointments""" # for appointment in appointments: # appointment.status = 'confirmed' # appointment.save() # # if form_data.get('notify_patients'): # self.send_confirmation_notification(appointment) # # def bulk_reschedule(self, appointments, form_data): # """Bulk reschedule appointments""" # new_date = form_data.get('bulk_date') # if new_date: # for appointment in appointments: # appointment.appointment_date = new_date # appointment.status = 'rescheduled' # appointment.save() # # if form_data.get('notify_patients'): # self.send_reschedule_notification(appointment, form_data) # # def bulk_cancel(self, appointments, form_data): # """Bulk cancel appointments""" # for appointment in appointments: # appointment.status = 'cancelled' # appointment.cancelled_at = timezone.now() # appointment.cancelled_by = self.request.user # appointment.cancellation_reason = 'bulk_cancellation' # appointment.cancellation_notes = form_data.get('bulk_reason', '') # appointment.save() # # if form_data.get('notify_patients'): # self.send_cancellation_notification(appointment, form_data) # # def bulk_send_reminders(self, appointments, form_data): # """Bulk send reminders""" # for appointment in appointments: # if form_data.get('notify_patients'): # self.send_reminder_notification(appointment) # # def send_confirmation_notification(self, appointment): # """Send confirmation notification""" # if appointment.patient.email: # send_mail( # subject='Appointment Confirmed', # message=f'Your appointment on {appointment.appointment_date} at {appointment.appointment_time} has been confirmed.', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def send_reminder_notification(self, appointment): # """Send reminder notification""" # if appointment.patient.email: # send_mail( # subject='Appointment Reminder', # message=f'Reminder: You have an appointment on {appointment.appointment_date} at {appointment.appointment_time}.', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Bulk Appointment Operations' # return context # # # class AppointmentListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # View for listing appointments # """ # model = Appointment # template_name = 'appointments/appointment_list.html' # context_object_name = 'appointments' # permission_required = 'appointments.view_appointment' # paginate_by = 25 # # def get_queryset(self): # queryset = Appointment.objects.filter(tenant=self.request.user.tenant) # # # Apply filters # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(provider__first_name__icontains=search) | # Q(provider__last_name__icontains=search) | # Q(reason__icontains=search) # ) # # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # date_from = self.request.GET.get('date_from') # if date_from: # queryset = queryset.filter(appointment_date__gte=date_from) # # date_to = self.request.GET.get('date_to') # if date_to: # queryset = queryset.filter(appointment_date__lte=date_to) # # provider = self.request.GET.get('provider') # if provider: # queryset = queryset.filter(provider_id=provider) # # return queryset.order_by('appointment_date', 'appointment_time') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Appointments' # context['providers'] = self.get_providers() # context['search'] = self.request.GET.get('search', '') # context['selected_status'] = self.request.GET.get('status', '') # context['selected_provider'] = self.request.GET.get('provider', '') # context['date_from'] = self.request.GET.get('date_from', '') # context['date_to'] = self.request.GET.get('date_to', '') # return context # # def get_providers(self): # """Get providers for filter""" # from django.contrib.auth.models import User # return User.objects.filter( # tenant=self.request.user.tenant, # groups__name__in=['Doctors', 'Nurses', 'Specialists'] # ).distinct() # # # class AppointmentDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # View for appointment details # """ # model = Appointment # template_name = 'appointments/appointment_detail.html' # context_object_name = 'appointment' # permission_required = 'appointments.view_appointment' # # def get_queryset(self): # return Appointment.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # appointment = self.object # context['title'] = f'Appointment - {appointment.patient.get_full_name()}' # context['confirmation'] = getattr(appointment, 'confirmation', None) # context['queue_entry'] = getattr(appointment, 'queue_entry', None) # context['telemedicine_session'] = getattr(appointment, 'telemedicine_session', None) # context['can_edit'] = self.request.user.has_perm('appointments.change_appointment') # context['can_cancel'] = self.request.user.has_perm('appointments.can_cancel_appointments') # context['can_reschedule'] = self.request.user.has_perm('appointments.can_reschedule_appointments') # return context # # # class QueueListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # View for listing queue entries # """ # model = Queue # template_name = 'appointments/queue_list.html' # context_object_name = 'queue_entries' # permission_required = 'appointments.view_queue' # # def get_queryset(self): # queryset = Queue.objects.filter(tenant=self.request.user.tenant) # # # Apply filters # status = self.request.GET.get('status', 'waiting') # if status: # queryset = queryset.filter(status=status) # # department = self.request.GET.get('department') # if department: # queryset = queryset.filter(department_id=department) # # return queryset.order_by('priority', 'created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Patient Queue' # context['departments'] = self.get_departments() # context['selected_status'] = self.request.GET.get('status', 'waiting') # context['selected_department'] = self.request.GET.get('department', '') # context['queue_stats'] = self.get_queue_stats() # return context # # def get_departments(self): # """Get departments for filter""" # from core.models import Department # return Department.objects.filter(tenant=self.request.user.tenant) # # def get_queue_stats(self): # """Get queue statistics""" # return { # 'total_waiting': Queue.objects.filter( # tenant=self.request.user.tenant, # status='waiting' # ).count(), # 'average_wait_time': 25, # Would be calculated # 'longest_wait': 45 # Would be calculated # } # # # # AJAX Views # @login_required # @permission_required('appointments.view_appointment') # def appointment_availability_ajax(request): # """AJAX view to check appointment availability""" # date = request.GET.get('date') # provider_id = request.GET.get('provider_id') # # if not date or not provider_id: # return JsonResponse({'success': False, 'message': 'Missing parameters'}) # # try: # from django.contrib.auth.models import User # provider = User.objects.get(id=provider_id, tenant=request.user.tenant) # appointment_date = datetime.strptime(date, '%Y-%m-%d').date() # # # Get existing appointments for the date # existing_appointments = Appointment.objects.filter( # provider=provider, # appointment_date=appointment_date, # status__in=['scheduled', 'confirmed', 'in_progress'] # ) # # # Generate available slots # available_slots = generate_available_slots(appointment_date, existing_appointments) # # return JsonResponse({ # 'success': True, # 'slots': available_slots # }) # except Exception as e: # return JsonResponse({'success': False, 'message': str(e)}) # # # @login_required # @permission_required('appointments.view_patient') # def patient_search_ajax(request): # """AJAX view for patient search""" # query = request.GET.get('q', '') # if len(query) < 2: # return JsonResponse({'patients': []}) # # patients = Patient.objects.filter( # tenant=request.user.tenant # ).filter( # Q(first_name__icontains=query) | # Q(last_name__icontains=query) | # Q(patient_id__icontains=query) | # Q(phone_number__icontains=query) # )[:10] # # patient_data = [ # { # 'id': patient.id, # 'name': patient.get_full_name(), # 'patient_id': patient.patient_id, # 'phone': patient.phone_number, # 'email': patient.email # } # for patient in patients # ] # # return JsonResponse({'patients': patient_data}) # # # @login_required # @permission_required('appointments.can_manage_queue') # def update_queue_position_ajax(request): # """AJAX view to update queue position""" # if request.method == 'POST': # try: # data = json.loads(request.body) # queue_id = data.get('queue_id') # new_position = data.get('new_position') # # queue_entry = Queue.objects.get( # id=queue_id, # tenant=request.user.tenant # ) # # queue_entry.position = new_position # queue_entry.save() # # return JsonResponse({ # 'success': True, # 'message': 'Queue position updated successfully.' # }) # except Queue.DoesNotExist: # return JsonResponse({ # 'success': False, # 'message': 'Queue entry not found.' # }) # except Exception as e: # return JsonResponse({ # 'success': False, # 'message': str(e) # }) # # return JsonResponse({'success': False, 'message': 'Invalid request.'}) # # # def generate_available_slots(date, existing_appointments): # """Generate available appointment slots for a date""" # slots = [] # start_time = datetime.strptime('09:00', '%H:%M').time() # end_time = datetime.strptime('17:00', '%H:%M').time() # slot_duration = 30 # minutes # # current_time = datetime.combine(date, start_time) # end_datetime = datetime.combine(date, end_time) # # while current_time < end_datetime: # slot_time = current_time.time() # # # Check if slot is available # is_available = True # for appointment in existing_appointments: # appointment_start = datetime.combine(date, appointment.appointment_time) # appointment_end = appointment_start + timedelta(minutes=appointment.duration) # # slot_start = current_time # slot_end = current_time + timedelta(minutes=slot_duration) # # if slot_start < appointment_end and slot_end > appointment_start: # is_available = False # break # # if is_available: # slots.append({ # 'time': slot_time.strftime('%H:%M'), # 'available': True # }) # # current_time += timedelta(minutes=slot_duration) # # return slots #