""" API endpoints for smart scheduling features. """ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.utils import timezone from datetime import datetime, timedelta from appointments.scheduling.smart_scheduler import SmartScheduler from appointments.scheduling.utils import SchedulingAnalytics, ConflictDetector from core.mixins import TenantMixin class SmartSchedulingViewSet(TenantMixin, viewsets.ViewSet): """API endpoints for smart scheduling features.""" permission_classes = [IsAuthenticated] @action(detail=False, methods=['post']) def find_optimal_slots(self, request): """ Find optimal appointment slots using AI. POST /api/appointments/scheduling/find-optimal-slots/ { "patient_id": 123, "provider_id": 456, "appointment_type_id": 789, "preferred_dates": ["2025-01-15", "2025-01-16"], "duration_minutes": 30 } """ # Extract parameters patient_id = request.data.get('patient_id') provider_id = request.data.get('provider_id') appointment_type_id = request.data.get('appointment_type_id') preferred_dates = request.data.get('preferred_dates', []) duration_minutes = request.data.get('duration_minutes', 30) # Validate inputs if not all([patient_id, provider_id, appointment_type_id]): return Response( {'error': 'Missing required parameters: patient_id, provider_id, appointment_type_id'}, status=status.HTTP_400_BAD_REQUEST ) # Get objects from patients.models import PatientProfile from accounts.models import User from appointments.models import AppointmentTemplate try: patient = PatientProfile.objects.get(id=patient_id, tenant=request.user.tenant) provider = User.objects.get(id=provider_id) appointment_type = AppointmentTemplate.objects.get(id=appointment_type_id, tenant=request.user.tenant) except PatientProfile.DoesNotExist: return Response( {'error': 'Patient not found'}, status=status.HTTP_404_NOT_FOUND ) except User.DoesNotExist: return Response( {'error': 'Provider not found'}, status=status.HTTP_404_NOT_FOUND ) except AppointmentTemplate.DoesNotExist: return Response( {'error': 'Appointment type not found'}, status=status.HTTP_404_NOT_FOUND ) # Convert date strings to datetime objects try: date_objects = [ datetime.strptime(date_str, '%Y-%m-%d') for date_str in preferred_dates ] except ValueError as e: return Response( {'error': f'Invalid date format: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST ) # Find optimal slots scheduler = SmartScheduler(request.user.tenant) slots = scheduler.find_optimal_slots( patient=patient, provider=provider, appointment_type=appointment_type, preferred_dates=date_objects, duration_minutes=duration_minutes ) # Format response return Response({ 'success': True, 'slots': [ { 'datetime': slot['datetime'].isoformat(), 'end_datetime': slot['end_datetime'].isoformat(), 'provider_id': slot['provider'].id, 'provider_name': slot['provider'].get_full_name(), 'score': slot['score']['total'], 'score_breakdown': slot['score']['breakdown'] } for slot in slots ], 'count': len(slots) }) @action(detail=False, methods=['post']) def check_conflicts(self, request): """ Check for scheduling conflicts. POST /api/appointments/scheduling/check-conflicts/ { "provider_id": 456, "start_time": "2025-01-15T10:00:00", "end_time": "2025-01-15T10:30:00", "exclude_appointment_id": 123 } """ provider_id = request.data.get('provider_id') start_time = request.data.get('start_time') end_time = request.data.get('end_time') exclude_id = request.data.get('exclude_appointment_id') # Validate inputs if not all([provider_id, start_time, end_time]): return Response( {'error': 'Missing required parameters: provider_id, start_time, end_time'}, status=status.HTTP_400_BAD_REQUEST ) # Get provider from accounts.models import User try: provider = User.objects.get(id=provider_id) except User.DoesNotExist: return Response( {'error': 'Provider not found'}, status=status.HTTP_404_NOT_FOUND ) # Parse times try: start_dt = datetime.fromisoformat(start_time) end_dt = datetime.fromisoformat(end_time) except ValueError as e: return Response( {'error': f'Invalid datetime format: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST ) # Check conflicts conflicts = ConflictDetector.check_conflicts( provider, start_dt, end_dt, exclude_id ) if conflicts: # Suggest alternatives alternatives = ConflictDetector.suggest_alternative_slots( provider, start_dt, duration_minutes=int((end_dt - start_dt).total_seconds() / 60) ) return Response({ 'has_conflict': True, 'conflicts': [ { 'id': c.id, 'patient': c.patient.get_full_name(), 'start': c.scheduled_datetime.isoformat(), 'end': c.scheduled_end_datetime.isoformat() } for c in conflicts ], 'alternative_slots': [ { 'start': alt['start'].isoformat(), 'end': alt['end'].isoformat() } for alt in alternatives ] }) return Response({ 'has_conflict': False, 'message': 'No conflicts found' }) @action(detail=False, methods=['get']) def provider_analytics(self, request): """ Get scheduling analytics for a provider. GET /api/appointments/scheduling/provider-analytics/?provider_id=456&days=30 """ provider_id = request.query_params.get('provider_id') days = int(request.query_params.get('days', 30)) if not provider_id: return Response( {'error': 'Missing required parameter: provider_id'}, status=status.HTTP_400_BAD_REQUEST ) from accounts.models import User try: provider = User.objects.get(id=provider_id) except User.DoesNotExist: return Response( {'error': 'Provider not found'}, status=status.HTTP_404_NOT_FOUND ) # Calculate date range end_date = timezone.now().date() start_date = end_date - timedelta(days=days) # Get analytics utilization = SchedulingAnalytics.calculate_provider_utilization( provider, start_date, end_date ) no_show_rate = SchedulingAnalytics.calculate_no_show_rate( provider, start_date, end_date ) return Response({ 'success': True, 'provider_id': provider.id, 'provider_name': provider.get_full_name(), 'period': { 'start': start_date.isoformat(), 'end': end_date.isoformat(), 'days': days }, 'metrics': { 'utilization_rate': utilization, 'no_show_rate': no_show_rate } }) @action(detail=False, methods=['post']) def update_patient_preferences(self, request): """ Update patient scheduling preferences based on history. POST /api/appointments/scheduling/update-patient-preferences/ { "patient_id": 123 } """ patient_id = request.data.get('patient_id') if not patient_id: return Response( {'error': 'Missing required parameter: patient_id'}, status=status.HTTP_400_BAD_REQUEST ) from patients.models import PatientProfile try: patient = PatientProfile.objects.get(id=patient_id, tenant=request.user.tenant) except PatientProfile.DoesNotExist: return Response( {'error': 'Patient not found'}, status=status.HTTP_404_NOT_FOUND ) # Update preferences prefs = SchedulingAnalytics.update_patient_scheduling_preferences(patient) if prefs: return Response({ 'success': True, 'message': 'Patient preferences updated successfully', 'preferences': { 'preferred_days': prefs.preferred_days, 'preferred_times': prefs.preferred_times, 'total_appointments': prefs.total_appointments, 'completed_appointments': prefs.completed_appointments, 'average_no_show_rate': float(prefs.average_no_show_rate), 'completion_rate': prefs.completion_rate } }) else: return Response({ 'success': False, 'message': 'No appointment history found for patient' })