alternative-backend-service/meetings/views.py

641 lines
24 KiB
Python
Raw Normal View History

from rest_framework import generics, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
from rest_framework.views import APIView
from django.utils import timezone
from datetime import datetime, timedelta
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, set_admin_availability, get_available_slots_for_week, check_date_availability
from .serializers import (
AdminWeeklyAvailabilitySerializer,
AdminWeeklyAvailabilityUpdateSerializer,
AppointmentRequestSerializer,
AppointmentRequestCreateSerializer,
AppointmentScheduleSerializer,
AppointmentRejectSerializer,
AvailabilityCheckSerializer,
AdminAvailabilityConfigSerializer,
AppointmentDetailSerializer,
MeetingJoinSerializer,
MeetingActionSerializer,
)
from .email_service import EmailService
from users.models import CustomUser
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated, IsAdminUser]
def get_serializer_class(self):
if self.request.method == 'GET':
return AdminWeeklyAvailabilitySerializer
return AdminWeeklyAvailabilityUpdateSerializer
def get_object(self):
return get_admin_availability()
def update(self, request, *args, **kwargs):
response = super().update(request, *args, **kwargs)
availability = self.get_object()
full_serializer = AdminWeeklyAvailabilitySerializer(availability)
return Response(full_serializer.data)
class AdminAvailabilityConfigView(generics.GenericAPIView):
permission_classes = [AllowAny]
def get(self, request):
"""Get availability configuration"""
config = AdminAvailabilityConfigSerializer.get_default_config()
return Response(config.data)
class AppointmentRequestListView(generics.ListAPIView):
serializer_class = AppointmentRequestSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
queryset = AppointmentRequest.objects.all()
if self.request.user.is_staff:
return queryset
return queryset.filter(email=self.request.user.email)
class AppointmentRequestCreateView(generics.CreateAPIView):
permission_classes = [IsAuthenticated]
queryset = AppointmentRequest.objects.all()
serializer_class = AppointmentRequestCreateSerializer
def perform_create(self, serializer):
appointment = serializer.save()
if appointment.are_preferences_available():
EmailService.send_admin_notification(appointment)
else:
EmailService.send_admin_notification(appointment, availability_mismatch=True)
class AppointmentRequestDetailView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated]
queryset = AppointmentRequest.objects.all()
serializer_class = AppointmentDetailSerializer
lookup_field = 'pk'
class ScheduleAppointmentView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, IsAdminUser]
serializer_class = AppointmentScheduleSerializer
queryset = AppointmentRequest.objects.all()
lookup_field = 'pk'
def post(self, request, pk):
try:
appointment = self.get_object()
if appointment.status != 'pending_review':
return Response(
{'error': 'Only pending review appointments can be scheduled.'},
status=status.HTTP_400_BAD_REQUEST
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
create_jitsi_meeting = serializer.validated_data.get('create_jitsi_meeting', True)
jitsi_custom_config = serializer.validated_data.get('jitsi_custom_config', {})
user_timezone = serializer.validated_data.get('timezone', 'UTC')
appointment.user_timezone = user_timezone
admin_user = request.user
appointment.schedule_appointment(
datetime_obj=serializer.validated_data['scheduled_datetime'],
duration=serializer.validated_data['scheduled_duration'],
create_meeting=create_jitsi_meeting,
moderator_user=admin_user,
commit=False
)
if jitsi_custom_config and create_jitsi_meeting:
if appointment.has_jitsi_meeting:
appointment.jitsi_meeting_config.update(jitsi_custom_config)
else:
appointment.create_jitsi_meeting(
moderator_user=admin_user,
with_moderation=True,
custom_config=jitsi_custom_config
)
appointment.save()
EmailService.send_appointment_scheduled(appointment)
response_serializer = AppointmentDetailSerializer(appointment)
participant_user = None
if appointment.email:
try:
participant_user = CustomUser.objects.get(email=appointment.email)
except CustomUser.DoesNotExist:
participant_user = None
return Response({
**response_serializer.data,
'message': 'Appointment scheduled successfully.',
'jitsi_meeting_created': appointment.has_jitsi_meeting,
'moderator_name': admin_user.get_full_name() or admin_user.username,
'moderator_join_url': appointment.get_moderator_join_url(
moderator_user=admin_user
) if appointment.has_jitsi_meeting else None,
'participant_join_url': appointment.get_participant_join_url(
participant_user=participant_user
) if appointment.has_jitsi_meeting and participant_user else None,
})
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class RejectAppointmentView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = AppointmentRejectSerializer
queryset = AppointmentRequest.objects.all()
lookup_field = 'pk'
def post(self, request, pk):
appointment = self.get_object()
if appointment.status != 'pending_review':
return Response(
{'error': 'Only pending appointments can be rejected'},
status=status.HTTP_400_BAD_REQUEST
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
appointment.reject_appointment(
serializer.validated_data.get('rejection_reason', '')
)
EmailService.send_appointment_rejected(appointment)
response_serializer = AppointmentDetailSerializer(appointment)
return Response(response_serializer.data)
class AvailableDatesView(generics.GenericAPIView):
permission_classes = [AllowAny]
def get(self, request):
availability = get_admin_availability()
if not availability or not availability.availability_schedule:
return Response([])
today = timezone.now().date()
available_dates = []
for i in range(1, 31):
date = today + timedelta(days=i)
day_of_week = date.weekday()
available_slots = availability.get_availability_for_day(day_of_week)
if available_slots:
available_dates.append({
'date': date.strftime('%Y-%m-%d'),
'day_name': date.strftime('%A'),
'available_slots': available_slots,
'available_slots_display': [
dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES).get(slot, slot)
for slot in available_slots
]
})
return Response(available_dates)
class CheckDateAvailabilityView(generics.GenericAPIView):
permission_classes = [AllowAny]
serializer_class = AvailabilityCheckSerializer
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
date_str = serializer.validated_data['date']
available_slots = check_date_availability(date_str)
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_name = date_obj.strftime('%A')
response_data = {
'date': date_str,
'day_name': day_name,
'available_slots': available_slots,
'available_slots_display': [
dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES).get(slot, slot)
for slot in available_slots
],
'is_available': len(available_slots) > 0
}
return Response(response_data)
except ValueError:
return Response(
{'error': 'Invalid date format'},
status=status.HTTP_400_BAD_REQUEST
)
class WeeklyAvailabilityView(generics.GenericAPIView):
permission_classes = [AllowAny]
def get(self, request):
availability = get_admin_availability()
if not availability:
return Response([])
weekly_availability = []
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
available_slots = availability.get_availability_for_day(day_num)
weekly_availability.append({
'day_number': day_num,
'day_name': day_name,
'available_slots': available_slots,
'available_slots_display': [
dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES).get(slot, slot)
for slot in available_slots
],
'is_available': len(available_slots) > 0
})
return Response(weekly_availability)
class UserAppointmentsView(generics.ListAPIView):
permission_classes = [IsAuthenticated]
serializer_class = AppointmentDetailSerializer
def get_queryset(self):
user_email = self.request.user.email.lower()
all_appointments = list(AppointmentRequest.objects.all())
matching_appointments = [
apt for apt in all_appointments
if apt.email and apt.email.lower() == user_email
]
appointment_ids = [apt.id for apt in matching_appointments]
return AppointmentRequest.objects.filter(
id__in=appointment_ids
).order_by('-created_at')
class AppointmentStatsView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, IsAdminUser]
def get(self, request):
total = AppointmentRequest.objects.count()
users = CustomUser.objects.filter(is_staff=False).count()
pending = AppointmentRequest.objects.filter(status='pending_review').count()
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
rejected = AppointmentRequest.objects.filter(status='rejected').count()
completed = AppointmentRequest.objects.filter(status='completed').count()
jitsi_meetings = AppointmentRequest.objects.filter(jitsi_meeting_created=True).count()
active_meetings = AppointmentRequest.objects.filter(
status='scheduled',
scheduled_datetime__gt=timezone.now()
).count()
availability = get_admin_availability()
availability_coverage = 0
if availability and availability.availability_schedule:
days_with_availability = len(availability.availability_schedule)
availability_coverage = round((days_with_availability / 7) * 100, 2)
return Response({
'total_requests': total,
'pending_review': pending,
'scheduled': scheduled,
'rejected': rejected,
'completed': completed,
'users': users,
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0,
'availability_coverage': availability_coverage,
'available_days_count': days_with_availability if availability else 0,
'jitsi_meetings_created': jitsi_meetings,
'active_upcoming_meetings': active_meetings,
'meetings_with_video': round((jitsi_meetings / total * 100), 2) if total > 0 else 0,
})
class UserAppointmentStatsView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = AppointmentDetailSerializer
def get_queryset(self):
user_email = self.request.user.email.lower()
all_appointments = list(AppointmentRequest.objects.all())
matching_appointments = [
apt for apt in all_appointments
if apt.email and apt.email.lower() == user_email
]
appointment_ids = [apt.id for apt in matching_appointments]
return AppointmentRequest.objects.filter(
id__in=appointment_ids
)
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
stats = {
'total': queryset.count(),
'pending': queryset.filter(status='pending_review').count(),
'scheduled': queryset.filter(status='scheduled').count(),
'rejected': queryset.filter(status='rejected').count(),
'completed': queryset.filter(status='completed').count(),
'video_meetings': queryset.filter(jitsi_meeting_created=True).count(),
}
total = stats['total']
scheduled = stats['scheduled']
completion_rate = round((scheduled / total * 100), 2) if total > 0 else 0
return Response({
'total_requests': total,
'pending_review': stats['pending'],
'scheduled': scheduled,
'rejected': stats['rejected'],
'completed': stats['completed'],
'video_meetings': stats['video_meetings'],
'completion_rate': completion_rate,
'email': request.user.email,
'upcoming_video_sessions': queryset.filter(
status='scheduled',
jitsi_meeting_created=True,
scheduled_datetime__gt=timezone.now()
).count(),
})
class MatchingAvailabilityView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk):
try:
appointment = AppointmentRequest.objects.get(pk=pk)
if not request.user.is_staff and appointment.email != request.user.email:
return Response(
{'error': 'You can only view your own appointments'},
status=status.HTTP_403_FORBIDDEN
)
matching_slots = appointment.get_matching_availability()
return Response({
'appointment_id': str(appointment.id),
'preferences_match_availability': appointment.are_preferences_available(),
'matching_slots': matching_slots,
'total_matching_slots': len(matching_slots)
})
except AppointmentRequest.DoesNotExist:
return Response(
{'error': 'Appointment not found'},
status=status.HTTP_404_NOT_FOUND
)
class StartMeetingView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = MeetingActionSerializer
queryset = AppointmentRequest.objects.all()
lookup_field = 'pk'
def post(self, request, pk):
appointment = self.get_object()
if not request.user.is_staff:
return Response(
{'error': 'Only staff members can start meetings'},
status=status.HTTP_403_FORBIDDEN
)
if appointment.status != 'scheduled':
return Response(
{'error': 'Only scheduled appointments can be started'},
status=status.HTTP_400_BAD_REQUEST
)
appointment.start_meeting()
return Response({
'appointment_id': str(appointment.id),
'message': 'Meeting started successfully',
'meeting_started_at': appointment.meeting_started_at,
})
class EndMeetingView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = MeetingActionSerializer
queryset = AppointmentRequest.objects.all()
lookup_field = 'pk'
def post(self, request, pk):
appointment = self.get_object()
if not request.user.is_staff:
return Response(
{'error': 'Only staff members can end meetings'},
status=status.HTTP_403_FORBIDDEN
)
if appointment.status != 'scheduled':
return Response(
{'error': 'Only scheduled appointments can be ended'},
status=status.HTTP_400_BAD_REQUEST
)
appointment.end_meeting()
return Response({
'appointment_id': str(appointment.id),
'message': 'Meeting ended successfully',
'meeting_ended_at': appointment.meeting_ended_at,
})
class JoinMeetingView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = MeetingJoinSerializer
queryset = AppointmentRequest.objects.all()
lookup_field = 'pk'
def get(self, request, pk):
appointment = self.get_object()
if not self._has_join_permission(request.user, appointment):
return Response(
{'error': 'You do not have permission to join this meeting'},
status=status.HTTP_403_FORBIDDEN
)
user_type = 'moderator' if request.user.is_staff else 'participant'
join_info = appointment.get_meeting_join_info(user_type)
return Response({
'appointment_id': str(appointment.id),
'user_type': user_type,
'user_name': request.user.get_full_name() or request.user.email,
'meeting_info': join_info,
'can_join_now': appointment.can_join_meeting(user_type),
'is_admin_join': appointment.is_admin_join,
'participant_join_enabled': appointment.is_admin_join if user_type == 'participant' else None,
})
def post(self, request, pk):
appointment = self.get_object()
if not self._has_join_permission(request.user, appointment):
return Response(
{'error': 'You do not have permission to join this meeting'},
status=status.HTTP_403_FORBIDDEN
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user_type = serializer.validated_data.get('user_type')
if request.user.is_staff and user_type != 'moderator':
user_type = 'moderator'
elif not request.user.is_staff and user_type == 'moderator':
return Response(
{'error': 'Only staff members can join as moderators'},
status=status.HTTP_403_FORBIDDEN
)
if user_type == 'moderator':
join_url = appointment.get_moderator_join_url(moderator_user=request.user)
else:
# Check if admin has joined and enabled participant joining
if not appointment.is_admin_join:
return Response(
{'error': 'Participants cannot join yet. Please wait for the admin to join and enable participant access.'},
status=status.HTTP_403_FORBIDDEN
)
join_url = appointment.get_participant_join_url(participant_user=request.user)
if not appointment.meeting_started_at and appointment.can_join_meeting(user_type):
appointment.start_meeting()
return Response({
'appointment_id': str(appointment.id),
'join_url': join_url,
'room_id': appointment.jitsi_room_id,
'user_type': user_type,
'password': appointment.jitsi_meeting_password if user_type == 'participant' else None,
'meeting_started': appointment.meeting_started_at is not None,
'join_instructions': self._get_join_instructions(user_type),
})
def _has_join_permission(self, user, appointment):
if user.is_staff:
return True
if appointment.email and appointment.email.lower() == user.email.lower():
return True
return False
def _get_join_instructions(self, user_type):
if user_type == 'moderator':
return {
'title': 'Join as Therapist/Moderator',
'instructions': [
'Click the join link above',
'You have full control over the meeting room',
'You can mute participants, lock the room, and manage recordings',
'Please join 15 minutes before the scheduled time',
'Wait for your patient in the lobby if enabled'
]
}
else:
return {
'title': 'Join as Patient/Participant',
'instructions': [
'Click the join link above',
'Enter your name when prompted',
'You may need to wait in the lobby until the therapist admits you',
'Please join 5 minutes before the scheduled time',
'Ensure you have a stable internet connection'
]
}
class MeetingAnalyticsView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated, IsAdminUser]
queryset = AppointmentRequest.objects.all()
serializer_class = AppointmentDetailSerializer
lookup_field = 'pk'
def retrieve(self, request, *args, **kwargs):
appointment = self.get_object()
analytics = appointment.get_meeting_analytics()
return Response({
'appointment_id': str(appointment.id),
'patient_name': appointment.full_name,
'analytics': analytics,
'video_meeting_info': {
'has_meeting': appointment.has_jitsi_meeting,
'room_id': appointment.jitsi_room_id,
'meeting_created': appointment.jitsi_meeting_created,
'recording_available': bool(appointment.jitsi_recording_url),
},
'timeline': {
'created': appointment.created_at.isoformat(),
'scheduled': appointment.scheduled_datetime.isoformat() if appointment.scheduled_datetime else None,
'started': appointment.meeting_started_at.isoformat() if appointment.meeting_started_at else None,
'ended': appointment.meeting_ended_at.isoformat() if appointment.meeting_ended_at else None,
}
})
@api_view(['GET'])
@permission_classes([AllowAny])
def availability_overview(request):
availability = get_admin_availability()
if not availability:
return Response({
'available': False,
'message': 'No availability set'
})
all_slots = availability.get_all_available_slots()
return Response({
'available': len(all_slots) > 0,
'total_available_slots': len(all_slots),
'available_days': list(set(slot['day_name'] for slot in all_slots)),
'next_available_dates': get_next_available_dates(7)
})
def get_next_available_dates(days_count=7):
availability = get_admin_availability()
if not availability:
return []
today = timezone.now().date()
next_dates = []
for i in range(1, days_count + 1):
date = today + timedelta(days=i)
day_of_week = date.weekday()
available_slots = availability.get_availability_for_day(day_of_week)
if available_slots:
next_dates.append({
'date': date.strftime('%Y-%m-%d'),
'day_name': date.strftime('%A'),
'available_slots': available_slots
})
return next_dates