296 lines
10 KiB
Python
296 lines
10 KiB
Python
"""
|
|
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'
|
|
})
|