Marwan Alwali 2780a2dc7c update
2025-09-16 15:10:57 +03:00

4739 lines
168 KiB
Python

"""
Appointments app views for hospital management system with comprehensive CRUD operations.
"""
from django.contrib.messages import success
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.template.defaulttags import csrf_token
from django.utils.dateparse import parse_datetime
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import (
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
)
from django.http import JsonResponse, HttpResponseBadRequest
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
#