Marwan Alwali 263292f6be update
2025-11-04 00:50:06 +03:00

513 lines
16 KiB
Python

"""
Utility functions for appointments app.
Provides helper functions for common operations.
"""
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from datetime import datetime, timedelta
import uuid
def get_tenant_from_request(request):
"""
Extract tenant from request object.
Args:
request: Django request object
Returns:
Tenant: Tenant object or None
Raises:
AttributeError: If request doesn't have user or tenant
"""
if not hasattr(request, 'user'):
return None
if not hasattr(request.user, 'tenant'):
return None
return request.user.tenant
def check_appointment_conflicts(provider, start_datetime, end_datetime, exclude_appointment_id=None):
"""
Check for appointment scheduling conflicts for a provider.
Args:
provider: User object (healthcare provider)
start_datetime: Proposed appointment start datetime
end_datetime: Proposed appointment end datetime
exclude_appointment_id: Optional appointment ID to exclude from conflict check
Returns:
dict: {
'has_conflict': bool,
'conflicting_appointments': QuerySet,
'message': str
}
"""
from .models import AppointmentRequest
# Query for overlapping appointments
conflicts = AppointmentRequest.objects.filter(
provider=provider,
status__in=['SCHEDULED', 'CONFIRMED', 'CHECKED_IN', 'IN_PROGRESS']
).filter(
scheduled_datetime__lt=end_datetime,
scheduled_end_datetime__gt=start_datetime
)
# Exclude specific appointment if provided (for rescheduling)
if exclude_appointment_id:
conflicts = conflicts.exclude(pk=exclude_appointment_id)
has_conflict = conflicts.exists()
result = {
'has_conflict': has_conflict,
'conflicting_appointments': conflicts,
'message': ''
}
if has_conflict:
conflict_count = conflicts.count()
result['message'] = (
f"Provider has {conflict_count} conflicting appointment(s) "
f"between {start_datetime.strftime('%Y-%m-%d %H:%M')} and "
f"{end_datetime.strftime('%Y-%m-%d %H:%M')}"
)
else:
result['message'] = "No scheduling conflicts found"
return result
def send_appointment_notification(appointment, notification_type, recipient=None):
"""
Send appointment notification to patient or provider.
Args:
appointment: AppointmentRequest object
notification_type: Type of notification ('confirmation', 'reminder', 'cancellation', 'reschedule')
recipient: Optional recipient ('patient' or 'provider'), defaults to 'patient'
Returns:
dict: {
'success': bool,
'method': str (email, sms, etc.),
'message': str
}
"""
if recipient is None:
recipient = 'patient'
result = {
'success': False,
'method': None,
'message': ''
}
# Determine recipient contact info
if recipient == 'patient':
contact_email = appointment.patient.email if hasattr(appointment.patient, 'email') else None
contact_phone = appointment.patient.phone_number if hasattr(appointment.patient, 'phone_number') else None
recipient_name = appointment.patient.get_full_name()
else: # provider
contact_email = appointment.provider.email
contact_phone = getattr(appointment.provider, 'phone_number', None)
recipient_name = appointment.provider.get_full_name()
# Prepare notification content based on type
subject, message = _prepare_notification_content(appointment, notification_type, recipient_name)
# Try to send via email first
if contact_email:
try:
from django.core.mail import send_mail
send_mail(
subject=subject,
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[contact_email],
fail_silently=False
)
result['success'] = True
result['method'] = 'email'
result['message'] = f"Email notification sent to {contact_email}"
return result
except Exception as e:
result['message'] = f"Email sending failed: {str(e)}"
# Fallback to SMS if email fails and phone is available
if contact_phone:
# SMS implementation would go here
# For now, just log that SMS would be sent
result['success'] = True
result['method'] = 'sms'
result['message'] = f"SMS notification would be sent to {contact_phone}"
return result
result['message'] = "No valid contact method available"
return result
def _prepare_notification_content(appointment, notification_type, recipient_name):
"""
Prepare notification subject and message content.
Args:
appointment: AppointmentRequest object
notification_type: Type of notification
recipient_name: Name of recipient
Returns:
tuple: (subject, message)
"""
tenant_name = appointment.tenant.name if appointment.tenant else "Hospital"
if notification_type == 'confirmation':
subject = f"Appointment Confirmation - {tenant_name}"
message = f"""
Dear {recipient_name},
Your appointment has been confirmed:
Date: {appointment.scheduled_datetime.strftime('%B %d, %Y')}
Time: {appointment.scheduled_datetime.strftime('%I:%M %p')}
Provider: {appointment.provider.get_full_name()}
Type: {appointment.get_appointment_type_display()}
Location: {appointment.location or 'To be determined'}
Please arrive 15 minutes early for check-in.
If you need to reschedule or cancel, please contact us at least 24 hours in advance.
Thank you,
{tenant_name}
"""
elif notification_type == 'reminder':
subject = f"Appointment Reminder - {tenant_name}"
message = f"""
Dear {recipient_name},
This is a reminder of your upcoming appointment:
Date: {appointment.scheduled_datetime.strftime('%B %d, %Y')}
Time: {appointment.scheduled_datetime.strftime('%I:%M %p')}
Provider: {appointment.provider.get_full_name()}
Location: {appointment.location or 'To be determined'}
Please arrive 15 minutes early for check-in.
Thank you,
{tenant_name}
"""
elif notification_type == 'cancellation':
subject = f"Appointment Cancelled - {tenant_name}"
message = f"""
Dear {recipient_name},
Your appointment has been cancelled:
Original Date: {appointment.scheduled_datetime.strftime('%B %d, %Y')}
Original Time: {appointment.scheduled_datetime.strftime('%I:%M %p')}
Reason: {appointment.cancellation_reason or 'Not specified'}
If you would like to reschedule, please contact us.
Thank you,
{tenant_name}
"""
elif notification_type == 'reschedule':
subject = f"Appointment Rescheduled - {tenant_name}"
message = f"""
Dear {recipient_name},
Your appointment has been rescheduled:
New Date: {appointment.scheduled_datetime.strftime('%B %d, %Y')}
New Time: {appointment.scheduled_datetime.strftime('%I:%M %p')}
Provider: {appointment.provider.get_full_name()}
Location: {appointment.location or 'To be determined'}
Please arrive 15 minutes early for check-in.
Thank you,
{tenant_name}
"""
else:
subject = f"Appointment Update - {tenant_name}"
message = f"""
Dear {recipient_name},
There has been an update to your appointment. Please contact us for details.
Thank you,
{tenant_name}
"""
return subject, message.strip()
def generate_meeting_url(appointment, platform=None):
"""
Generate telemedicine meeting URL for an appointment.
Args:
appointment: AppointmentRequest object
platform: Optional platform override ('ZOOM', 'TEAMS', 'WEBEX', 'DOXY', 'CUSTOM')
Returns:
dict: {
'success': bool,
'meeting_url': str,
'meeting_id': str,
'meeting_password': str,
'platform': str,
'message': str
}
"""
if platform is None:
platform = appointment.telemedicine_platform or 'CUSTOM'
result = {
'success': False,
'meeting_url': None,
'meeting_id': None,
'meeting_password': None,
'platform': platform,
'message': ''
}
# Generate unique meeting ID
meeting_id = f"{appointment.request_id.hex[:8]}-{uuid.uuid4().hex[:8]}"
# Generate meeting password
meeting_password = uuid.uuid4().hex[:12].upper()
# Generate platform-specific URLs
if platform == 'ZOOM':
# In production, this would integrate with Zoom API
meeting_url = f"https://zoom.us/j/{meeting_id}"
result['message'] = "Zoom meeting URL generated (mock)"
elif platform == 'TEAMS':
# In production, this would integrate with Microsoft Teams API
meeting_url = f"https://teams.microsoft.com/l/meetup-join/{meeting_id}"
result['message'] = "Microsoft Teams meeting URL generated (mock)"
elif platform == 'WEBEX':
# In production, this would integrate with Webex API
meeting_url = f"https://webex.com/meet/{meeting_id}"
result['message'] = "Webex meeting URL generated (mock)"
elif platform == 'DOXY':
# Doxy.me uses simple room URLs
room_name = f"{appointment.provider.last_name.lower()}-{meeting_id[:8]}"
meeting_url = f"https://doxy.me/{room_name}"
meeting_password = None # Doxy.me doesn't use passwords by default
result['message'] = "Doxy.me meeting URL generated"
else: # CUSTOM or OTHER
# Generic meeting URL
meeting_url = f"https://telehealth.{appointment.tenant.domain if hasattr(appointment.tenant, 'domain') else 'hospital.com'}/meet/{meeting_id}"
result['message'] = "Custom meeting URL generated"
result.update({
'success': True,
'meeting_url': meeting_url,
'meeting_id': meeting_id,
'meeting_password': meeting_password
})
return result
def calculate_appointment_duration(appointment_type, specialty=None):
"""
Calculate recommended appointment duration based on type and specialty.
Args:
appointment_type: Type of appointment
specialty: Optional medical specialty
Returns:
int: Recommended duration in minutes
"""
# Default durations by appointment type
duration_map = {
'CONSULTATION': 30,
'FOLLOW_UP': 15,
'PROCEDURE': 60,
'SURGERY': 120,
'DIAGNOSTIC': 45,
'THERAPY': 60,
'VACCINATION': 15,
'SCREENING': 30,
'EMERGENCY': 30,
'TELEMEDICINE': 20,
'OTHER': 30
}
# Specialty-specific adjustments
specialty_adjustments = {
'SURGERY': 30,
'PSYCHIATRY': 15,
'CARDIOLOGY': 15,
'ONCOLOGY': 15,
}
base_duration = duration_map.get(appointment_type, 30)
if specialty and specialty in specialty_adjustments:
base_duration += specialty_adjustments[specialty]
return base_duration
def validate_appointment_time(scheduled_datetime, tenant):
"""
Validate if appointment time is within operating hours.
Args:
scheduled_datetime: Proposed appointment datetime
tenant: Tenant object
Returns:
dict: {
'valid': bool,
'message': str
}
"""
# Default operating hours (8 AM to 6 PM)
operating_start = 8
operating_end = 18
# Check if tenant has custom operating hours
if hasattr(tenant, 'operating_hours'):
day_of_week = scheduled_datetime.weekday()
if str(day_of_week) in tenant.operating_hours:
hours = tenant.operating_hours[str(day_of_week)]
if hours.get('enabled'):
operating_start = int(hours.get('start_time', '08:00').split(':')[0])
operating_end = int(hours.get('end_time', '18:00').split(':')[0])
appointment_hour = scheduled_datetime.hour
if appointment_hour < operating_start or appointment_hour >= operating_end:
return {
'valid': False,
'message': f"Appointment time must be between {operating_start}:00 and {operating_end}:00"
}
# Check if appointment is in the past
if scheduled_datetime < timezone.now():
return {
'valid': False,
'message': "Appointment cannot be scheduled in the past"
}
# Check if appointment is on a weekend (optional)
if scheduled_datetime.weekday() in [5, 6]: # Saturday, Sunday
return {
'valid': False,
'message': "Appointments cannot be scheduled on weekends"
}
return {
'valid': True,
'message': "Appointment time is valid"
}
def get_available_time_slots(provider, date, duration_minutes=30):
"""
Get available time slots for a provider on a specific date.
Args:
provider: User object (healthcare provider)
date: Date to check availability
duration_minutes: Duration of appointment in minutes
Returns:
list: List of available time slots as datetime objects
"""
from .models import AppointmentRequest, SlotAvailability
available_slots = []
# Get provider's availability slots for the date
slots = SlotAvailability.objects.filter(
provider=provider,
date=date,
is_active=True,
is_blocked=False
).order_by('start_time')
for slot in slots:
if slot.available_capacity > 0:
# Create datetime from date and time
slot_datetime = datetime.combine(date, slot.start_time)
slot_datetime = timezone.make_aware(slot_datetime)
# Check if there's a conflict
end_datetime = slot_datetime + timedelta(minutes=duration_minutes)
conflict_check = check_appointment_conflicts(
provider,
slot_datetime,
end_datetime
)
if not conflict_check['has_conflict']:
available_slots.append(slot_datetime)
return available_slots
def format_appointment_summary(appointment):
"""
Format appointment details as a summary string.
Args:
appointment: AppointmentRequest object
Returns:
str: Formatted appointment summary
"""
summary = f"""
Appointment Summary
==================
Patient: {appointment.patient.get_full_name()}
MRN: {appointment.patient.mrn if hasattr(appointment.patient, 'mrn') else 'N/A'}
Provider: {appointment.provider.get_full_name()}
Type: {appointment.get_appointment_type_display()}
Specialty: {appointment.get_specialty_display()}
Date: {appointment.scheduled_datetime.strftime('%B %d, %Y') if appointment.scheduled_datetime else 'Not scheduled'}
Time: {appointment.scheduled_datetime.strftime('%I:%M %p') if appointment.scheduled_datetime else 'Not scheduled'}
Duration: {appointment.duration_minutes} minutes
Location: {appointment.location or 'Not specified'}
Status: {appointment.get_status_display()}
Priority: {appointment.get_priority_display()}
"""
if appointment.is_telemedicine:
summary += f"\nTelemedicine: Yes"
summary += f"\nPlatform: {appointment.get_telemedicine_platform_display() if appointment.telemedicine_platform else 'Not specified'}"
if appointment.meeting_url:
summary += f"\nMeeting URL: {appointment.meeting_url}"
if appointment.chief_complaint:
summary += f"\nChief Complaint: {appointment.chief_complaint}"
return summary.strip()