836 lines
32 KiB
Python
836 lines
32 KiB
Python
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,
|
|
AvailabilityResponseSerializer,
|
|
WeeklyAvailabilitySerializer,
|
|
AdminAvailabilityConfigSerializer,
|
|
AppointmentDetailSerializer,
|
|
JitsiMeetingSerializer,
|
|
MeetingJoinSerializer,
|
|
MeetingActionSerializer
|
|
)
|
|
from .email_service import EmailService
|
|
from users.models import CustomUser
|
|
from django.db.models import Count, Q
|
|
import hashlib
|
|
|
|
|
|
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):
|
|
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', {})
|
|
|
|
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)
|
|
|
|
# Try to get participant user from appointment email
|
|
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,
|
|
})
|
|
|
|
|
|
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 MeetingActionView(generics.GenericAPIView):
|
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
serializer_class = MeetingActionSerializer
|
|
queryset = AppointmentRequest.objects.all()
|
|
lookup_field = 'pk'
|
|
|
|
def post(self, request, pk):
|
|
appointment = self.get_object()
|
|
|
|
if appointment.status != 'scheduled':
|
|
return Response(
|
|
{'error': 'Meeting actions only available for scheduled appointments'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
action = serializer.validated_data['action']
|
|
|
|
if action == 'start':
|
|
appointment.start_meeting()
|
|
message = 'Meeting started successfully'
|
|
|
|
elif action == 'end':
|
|
appointment.end_meeting()
|
|
message = 'Meeting ended successfully'
|
|
|
|
elif action == 'update_metadata':
|
|
metadata = serializer.validated_data.get('metadata', {})
|
|
appointment.update_meeting_data(metadata)
|
|
message = 'Meeting metadata updated successfully'
|
|
|
|
elif action == 'record':
|
|
recording_url = serializer.validated_data.get('recording_url', '')
|
|
if recording_url:
|
|
appointment.jitsi_recording_url = recording_url
|
|
appointment.save(update_fields=['jitsi_recording_url'])
|
|
message = 'Recording URL saved successfully'
|
|
else:
|
|
return Response(
|
|
{'error': 'Recording URL is required for record action'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
elif action == 'allow_participants':
|
|
appointment.is_admin_join = True
|
|
appointment.save(update_fields=['is_admin_join', 'updated_at'])
|
|
message = 'Participants can now join the meeting'
|
|
|
|
elif action == 'disallow_participants':
|
|
appointment.is_admin_join = False
|
|
appointment.save(update_fields=['is_admin_join', 'updated_at'])
|
|
message = 'Participants are no longer allowed to join the meeting'
|
|
|
|
return Response({
|
|
'appointment_id': str(appointment.id),
|
|
'action': action,
|
|
'message': message,
|
|
'meeting_status': appointment.status,
|
|
'started_at': appointment.meeting_started_at,
|
|
'ended_at': appointment.meeting_ended_at,
|
|
'is_admin_join': appointment.is_admin_join,
|
|
})
|
|
|
|
|
|
class UpcomingMeetingsView(generics.ListAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = AppointmentDetailSerializer
|
|
|
|
def get_queryset(self):
|
|
queryset = AppointmentRequest.objects.filter(
|
|
status='scheduled',
|
|
scheduled_datetime__gt=timezone.now()
|
|
).order_by('scheduled_datetime')
|
|
|
|
if not self.request.user.is_staff:
|
|
user_email = self.request.user.email.lower()
|
|
all_appointments = list(queryset)
|
|
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]
|
|
queryset = queryset.filter(id__in=appointment_ids)
|
|
|
|
return queryset
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
response = super().list(request, *args, **kwargs)
|
|
queryset = self.get_queryset()
|
|
now = timezone.now()
|
|
|
|
upcoming_meetings = []
|
|
for meeting in queryset:
|
|
meeting_data = {
|
|
'id': str(meeting.id),
|
|
'title': f"Session with {meeting.full_name}",
|
|
'start': meeting.scheduled_datetime.isoformat(),
|
|
'end': (meeting.scheduled_datetime + timedelta(minutes=meeting.scheduled_duration)).isoformat(),
|
|
'can_join': meeting.can_join_meeting('moderator' if request.user.is_staff else 'participant'),
|
|
'has_video': meeting.has_jitsi_meeting,
|
|
'status': meeting.get_meeting_status(),
|
|
}
|
|
upcoming_meetings.append(meeting_data)
|
|
|
|
response.data = {
|
|
'upcoming_meetings': upcoming_meetings,
|
|
'total_upcoming': queryset.count(),
|
|
'next_meeting': upcoming_meetings[0] if upcoming_meetings else None,
|
|
'now': now.isoformat(),
|
|
}
|
|
|
|
return response
|
|
|
|
|
|
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,
|
|
}
|
|
})
|
|
|
|
|
|
class BulkMeetingActionsView(generics.GenericAPIView):
|
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
|
|
def post(self, request):
|
|
action = request.data.get('action')
|
|
appointment_ids = request.data.get('appointment_ids', [])
|
|
|
|
if not action or not appointment_ids:
|
|
return Response(
|
|
{'error': 'Action and appointment_ids are required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
valid_actions = ['create_jitsi_meetings', 'send_reminders', 'end_old_meetings']
|
|
if action not in valid_actions:
|
|
return Response(
|
|
{'error': f'Invalid action. Must be one of: {valid_actions}'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
appointments = AppointmentRequest.objects.filter(
|
|
id__in=appointment_ids,
|
|
status='scheduled'
|
|
)
|
|
|
|
results = []
|
|
|
|
if action == 'create_jitsi_meetings':
|
|
for appointment in appointments:
|
|
if not appointment.has_jitsi_meeting:
|
|
appointment.create_jitsi_meeting()
|
|
results.append({
|
|
'id': str(appointment.id),
|
|
'action': 'meeting_created',
|
|
'success': True,
|
|
'message': f'Jitsi meeting created for {appointment.full_name}',
|
|
})
|
|
else:
|
|
results.append({
|
|
'id': str(appointment.id),
|
|
'action': 'already_created',
|
|
'success': True,
|
|
'message': f'Jitsi meeting already exists for {appointment.full_name}',
|
|
})
|
|
|
|
elif action == 'send_reminders':
|
|
for appointment in appointments:
|
|
if appointment.has_jitsi_meeting and appointment.meeting_in_future:
|
|
EmailService.send_meeting_reminder(appointment)
|
|
results.append({
|
|
'id': str(appointment.id),
|
|
'action': 'reminder_sent',
|
|
'success': True,
|
|
'message': f'Reminder sent to {appointment.full_name}',
|
|
})
|
|
|
|
elif action == 'end_old_meetings':
|
|
for appointment in appointments:
|
|
if appointment.meeting_started_at and not appointment.meeting_ended_at:
|
|
scheduled_end = appointment.scheduled_datetime + timedelta(minutes=appointment.scheduled_duration)
|
|
buffer_end = scheduled_end + timedelta(minutes=30)
|
|
|
|
if timezone.now() > buffer_end:
|
|
appointment.end_meeting()
|
|
results.append({
|
|
'id': str(appointment.id),
|
|
'action': 'meeting_ended',
|
|
'success': True,
|
|
'message': f'Meeting ended for {appointment.full_name}',
|
|
})
|
|
|
|
return Response({
|
|
'action': action,
|
|
'total_processed': len(results),
|
|
'results': results,
|
|
})
|
|
|
|
|
|
@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 |