""" Appointments app models for hospital management system. Provides appointment scheduling, queue management, and telemedicine functionality. """ import uuid from django.db import models from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator from django.utils import timezone from django.conf import settings from datetime import timedelta, datetime, time import json class AppointmentRequest(models.Model): """ Appointment request model for scheduling patient appointments. """ class AppointmentType(models.TextChoices): CONSULTATION = 'CONSULTATION', 'Consultation' FOLLOW_UP = 'FOLLOW_UP', 'Follow-up' PROCEDURE = 'PROCEDURE', 'Procedure' SURGERY = 'SURGERY', 'Surgery' DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic' THERAPY = 'THERAPY', 'Therapy' VACCINATION = 'VACCINATION', 'Vaccination' SCREENING = 'SCREENING', 'Screening' EMERGENCY = 'EMERGENCY', 'Emergency' TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine' OTHER = 'OTHER', 'Other' class Specialty(models.TextChoices): FAMILY_MEDICINE = 'FAMILY_MEDICINE', 'Family Medicine' INTERNAL_MEDICINE = 'INTERNAL_MEDICINE', 'Internal Medicine' PEDIATRICS = 'PEDIATRICS', 'Pediatrics' CARDIOLOGY = 'CARDIOLOGY', 'Cardiology' DERMATOLOGY = 'DERMATOLOGY', 'Dermatology' ENDOCRINOLOGY = 'ENDOCRINOLOGY', 'Endocrinology' GASTROENTEROLOGY = 'GASTROENTEROLOGY', 'Gastroenterology' NEUROLOGY = 'NEUROLOGY', 'Neurology' ONCOLOGY = 'ONCOLOGY', 'Oncology' ORTHOPEDICS = 'ORTHOPEDICS', 'Orthopedics' PSYCHIATRY = 'PSYCHIATRY', 'Psychiatry' RADIOLOGY = 'RADIOLOGY', 'Radiology' SURGERY = 'SURGERY', 'Surgery' UROLOGY = 'UROLOGY', 'Urology' GYNECOLOGY = 'GYNECOLOGY', 'Gynecology' OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology' ENT = 'ENT', 'Ear, Nose & Throat' EMERGENCY = 'EMERGENCY', 'Emergency Medicine' OTHER = 'OTHER', 'Other' class Priority(models.TextChoices): ROUTINE = 'ROUTINE', 'Routine' URGENT = 'URGENT', 'Urgent' STAT = 'STAT', 'STAT' EMERGENCY = 'EMERGENCY', 'Emergency' class AppointmentStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' SCHEDULED = 'SCHEDULED', 'Scheduled' CONFIRMED = 'CONFIRMED', 'Confirmed' CHECKED_IN = 'CHECKED_IN', 'Checked In' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' NO_SHOW = 'NO_SHOW', 'No Show' RESCHEDULED = 'RESCHEDULED', 'Rescheduled' class TelemedicinePlatform(models.TextChoices): ZOOM = 'ZOOM', 'Zoom' TEAMS = 'TEAMS', 'Microsoft Teams' WEBEX = 'WEBEX', 'Cisco Webex' DOXY = 'DOXY', 'Doxy.me' CUSTOM = 'CUSTOM', 'Custom Platform' OTHER = 'OTHER', 'Other' # Basic Identifiers request_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique appointment request identifier' ) # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='appointment_requests', help_text='Organization tenant' ) # Patient Information patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='appointment_requests', help_text='Patient requesting appointment' ) # Provider Information provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='provider_appointments', help_text='Healthcare provider' ) # Appointment Details appointment_type = models.CharField( max_length=50, choices=AppointmentType.choices, help_text='Type of appointment' ) specialty = models.CharField( max_length=100, choices=Specialty.choices, help_text='Medical specialty' ) # Scheduling Information preferred_date = models.DateField( help_text='Patient preferred appointment date' ) preferred_time = models.TimeField( blank=True, null=True, help_text='Patient preferred appointment time' ) duration_minutes = models.PositiveIntegerField( default=30, validators=[MinValueValidator(15), MaxValueValidator(480)], help_text='Appointment duration in minutes' ) # Scheduling Flexibility flexible_scheduling = models.BooleanField( default=True, help_text='Patient accepts alternative times' ) earliest_acceptable_date = models.DateField( blank=True, null=True, help_text='Earliest acceptable appointment date' ) latest_acceptable_date = models.DateField( blank=True, null=True, help_text='Latest acceptable appointment date' ) acceptable_times = models.JSONField( default=list, blank=True, help_text='Acceptable time slots (JSON array)' ) # Priority and Urgency priority = models.CharField( max_length=20, choices=Priority.choices, default=Priority.ROUTINE, help_text='Appointment priority' ) urgency_score = models.PositiveIntegerField( default=1, validators=[MinValueValidator(1), MaxValueValidator(10)], help_text='Urgency score (1-10, 10 being most urgent)' ) # Clinical Information chief_complaint = models.TextField( help_text='Patient chief complaint or reason for visit' ) clinical_notes = models.TextField( blank=True, null=True, help_text='Additional clinical notes' ) referring_provider = models.CharField( max_length=200, blank=True, null=True, help_text='Referring provider name' ) # Insurance and Authorization insurance_verified = models.BooleanField( default=False, help_text='Insurance coverage verified' ) authorization_required = models.BooleanField( default=False, help_text='Prior authorization required' ) authorization_number = models.CharField( max_length=100, blank=True, null=True, help_text='Authorization number' ) # Status and Workflow status = models.CharField( max_length=20, choices=AppointmentStatus.choices, default=AppointmentStatus.PENDING, help_text='Appointment status' ) # Scheduled Information scheduled_datetime = models.DateTimeField( blank=True, null=True, help_text='Scheduled appointment date and time' ) scheduled_end_datetime = models.DateTimeField( blank=True, null=True, help_text='Scheduled appointment end time' ) # Location Information location = models.CharField( max_length=200, blank=True, null=True, help_text='Appointment location' ) room_number = models.CharField( max_length=50, blank=True, null=True, help_text='Room number' ) # Telemedicine Information is_telemedicine = models.BooleanField( default=False, help_text='Telemedicine appointment' ) telemedicine_platform = models.CharField( max_length=50, choices=TelemedicinePlatform.choices, blank=True, null=True, help_text='Telemedicine platform' ) meeting_url = models.URLField( blank=True, null=True, help_text='Telemedicine meeting URL' ) meeting_id = models.CharField( max_length=100, blank=True, null=True, help_text='Meeting ID or room number' ) meeting_password = models.CharField( max_length=100, blank=True, null=True, help_text='Meeting password' ) # Check-in Information checked_in_at = models.DateTimeField( blank=True, null=True, help_text='Patient check-in time' ) checked_in_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='checked_in_appointments', help_text='Staff member who checked in patient' ) # Completion Information completed_at = models.DateTimeField( blank=True, null=True, help_text='Appointment completion time' ) actual_duration_minutes = models.PositiveIntegerField( blank=True, null=True, help_text='Actual appointment duration' ) # Cancellation Information cancelled_at = models.DateTimeField( blank=True, null=True, help_text='Cancellation timestamp' ) cancelled_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='cancelled_appointments', help_text='User who cancelled appointment' ) cancellation_reason = models.TextField( blank=True, null=True, help_text='Reason for cancellation' ) # Rescheduling Information rescheduled_from = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='rescheduled_appointments', help_text='Original appointment if rescheduled' ) # Communication Preferences reminder_preferences = models.JSONField( default=dict, blank=True, help_text='Reminder preferences (email, SMS, phone)' ) # Special Requirements special_requirements = models.TextField( blank=True, null=True, help_text='Special requirements or accommodations' ) interpreter_needed = models.BooleanField( default=False, help_text='Interpreter services needed' ) interpreter_language = models.CharField( max_length=50, blank=True, null=True, help_text='Required interpreter language' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_appointments', help_text='User who created the appointment request' ) class Meta: db_table = 'appointments_appointment_request' verbose_name = 'Appointment Request' verbose_name_plural = 'Appointment Requests' ordering = ['-created_at'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'status']), models.Index(fields=['provider', 'scheduled_datetime']), models.Index(fields=['scheduled_datetime']), models.Index(fields=['priority', 'urgency_score']), models.Index(fields=['appointment_type', 'specialty']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.appointment_type} ({self.status})" @property def is_overdue(self): """ Check if appointment is overdue. """ if self.scheduled_datetime and self.status in ['SCHEDULED', 'CONFIRMED']: return timezone.now() > self.scheduled_datetime return False @property def wait_time_minutes(self): """ Calculate wait time if checked in. """ if self.checked_in_at and self.status == 'CHECKED_IN': return int((timezone.now() - self.checked_in_at).total_seconds() / 60) return None def check_in(self, checked_in_by=None): """ Mark appointment as checked in. Args: checked_in_by: User who checked in the patient Returns: bool: True if successful, False otherwise """ if self.status not in ['SCHEDULED', 'CONFIRMED']: return False self.status = 'CHECKED_IN' self.checked_in_at = timezone.now() if checked_in_by: self.checked_in_by = checked_in_by self.save(update_fields=['status', 'checked_in_at', 'checked_in_by', 'updated_at']) return True def start(self): """ Start the appointment. Returns: bool: True if successful, False otherwise """ if self.status not in ['CHECKED_IN', 'CONFIRMED']: return False self.status = 'IN_PROGRESS' if not self.checked_in_at: self.checked_in_at = timezone.now() self.save(update_fields=['status', 'checked_in_at', 'updated_at']) return True def complete(self): """ Complete the appointment. Returns: bool: True if successful, False otherwise """ if self.status != 'IN_PROGRESS': return False self.status = 'COMPLETED' self.completed_at = timezone.now() # Calculate actual duration if checked in if self.checked_in_at: duration = (self.completed_at - self.checked_in_at).total_seconds() / 60 self.actual_duration_minutes = int(duration) self.save(update_fields=['status', 'completed_at', 'actual_duration_minutes', 'updated_at']) return True def cancel(self, reason, cancelled_by=None): """ Cancel the appointment. Args: reason: Reason for cancellation cancelled_by: User who cancelled the appointment Returns: bool: True if successful, False otherwise """ if self.status in ['COMPLETED', 'CANCELLED']: return False self.status = 'CANCELLED' self.cancelled_at = timezone.now() self.cancellation_reason = reason if cancelled_by: self.cancelled_by = cancelled_by self.save(update_fields=['status', 'cancelled_at', 'cancellation_reason', 'cancelled_by', 'updated_at']) return True class SlotAvailability(models.Model): """ Provider availability slots for appointment scheduling. """ class AvailabilityType(models.TextChoices): REGULAR = 'REGULAR', 'Regular Hours' EXTENDED = 'EXTENDED', 'Extended Hours' EMERGENCY = 'EMERGENCY', 'Emergency' ON_CALL = 'ON_CALL', 'On Call' TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine Only' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='availability_slots', help_text='Organization tenant' ) # Provider Information provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='availability_slots', help_text='Healthcare provider' ) # Slot Information slot_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique slot identifier' ) # Date and Time date = models.DateField( help_text='Availability date' ) start_time = models.TimeField( help_text='Slot start time' ) end_time = models.TimeField( help_text='Slot end time' ) duration_minutes = models.PositiveIntegerField( help_text='Slot duration in minutes' ) # Availability Type availability_type = models.CharField( max_length=20, choices=AvailabilityType.choices, default=AvailabilityType.REGULAR, help_text='Type of availability' ) # Capacity and Booking max_appointments = models.PositiveIntegerField( default=1, help_text='Maximum appointments for this slot' ) booked_appointments = models.PositiveIntegerField( default=0, help_text='Number of booked appointments' ) # Location Information location = models.CharField( max_length=200, help_text='Appointment location' ) room_number = models.CharField( max_length=50, blank=True, null=True, help_text='Room number' ) # Specialty and Services specialty = models.CharField( max_length=100, help_text='Medical specialty for this slot' ) appointment_types = models.JSONField( default=list, help_text='Allowed appointment types for this slot' ) # Restrictions patient_restrictions = models.JSONField( default=dict, blank=True, help_text='Patient restrictions (age, gender, etc.)' ) insurance_restrictions = models.JSONField( default=list, blank=True, help_text='Accepted insurance types' ) # Telemedicine Support supports_telemedicine = models.BooleanField( default=False, help_text='Slot supports telemedicine appointments' ) telemedicine_only = models.BooleanField( default=False, help_text='Telemedicine only slot' ) # Status is_active = models.BooleanField( default=True, help_text='Slot is active and bookable' ) is_blocked = models.BooleanField( default=False, help_text='Slot is temporarily blocked' ) block_reason = models.CharField( max_length=200, blank=True, null=True, help_text='Reason for blocking slot' ) # Recurring Pattern is_recurring = models.BooleanField( default=False, help_text='Slot is part of recurring pattern' ) recurrence_pattern = models.JSONField( default=dict, blank=True, help_text='Recurrence pattern configuration' ) recurrence_end_date = models.DateField( blank=True, null=True, help_text='End date for recurring pattern' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_availability_slots', help_text='User who created the slot' ) class Meta: db_table = 'appointments_slot_availability' verbose_name = 'Slot Availability' verbose_name_plural = 'Slot Availability' ordering = ['date', 'start_time'] indexes = [ models.Index(fields=['tenant', 'provider', 'date']), models.Index(fields=['date', 'start_time']), models.Index(fields=['specialty']), models.Index(fields=['is_active', 'is_blocked']), ] unique_together = ['provider', 'date', 'start_time'] def __str__(self): return f"{self.provider.get_full_name()} - {self.date} {self.start_time}-{self.end_time}" @property def is_available(self): """ Check if slot has availability. """ return ( self.is_active and not self.is_blocked and self.booked_appointments < self.max_appointments ) @property def available_capacity(self): """ Get available capacity for slot. """ return max(0, self.max_appointments - self.booked_appointments) class WaitingQueue(models.Model): """ Waiting queue for managing patient flow. """ class QueueType(models.TextChoices): PROVIDER = 'PROVIDER', 'Provider Queue' SPECIALTY = 'SPECIALTY', 'Specialty Queue' LOCATION = 'LOCATION', 'Location Queue' PROCEDURE = 'PROCEDURE', 'Procedure Queue' EMERGENCY = 'EMERGENCY', 'Emergency Queue' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='waiting_queues', help_text='Organization tenant' ) # Queue Information queue_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique queue identifier' ) name = models.CharField( max_length=200, help_text='Queue name' ) description = models.TextField( blank=True, null=True, help_text='Queue description' ) # Queue Type and Configuration queue_type = models.CharField( max_length=20, choices=QueueType.choices, help_text='Type of queue' ) # Associated Resources providers = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='waiting_queues', blank=True, help_text='Providers associated with this queue' ) specialty = models.CharField( max_length=100, blank=True, null=True, help_text='Medical specialty' ) location = models.CharField( max_length=200, blank=True, null=True, help_text='Queue location' ) # Queue Management max_queue_size = models.PositiveIntegerField( default=50, help_text='Maximum queue size' ) average_service_time_minutes = models.PositiveIntegerField( default=30, help_text='Average service time in minutes' ) # Priority Configuration priority_weights = models.JSONField( default=dict, help_text='Priority weights for queue ordering' ) # Status is_active = models.BooleanField( default=True, help_text='Queue is active' ) is_accepting_patients = models.BooleanField( default=True, help_text='Queue is accepting new patients' ) # Operating Hours operating_hours = models.JSONField( default=dict, help_text='Queue operating hours by day' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_waiting_queues', help_text='User who created the queue' ) class Meta: db_table = 'appointments_waiting_queue' verbose_name = 'Waiting Queue' verbose_name_plural = 'Waiting Queues' ordering = ['name'] indexes = [ models.Index(fields=['tenant', 'queue_type']), models.Index(fields=['specialty']), models.Index(fields=['is_active']), ] def __str__(self): return f"{self.name} ({self.queue_type})" @property def current_queue_size(self): """ Get current queue size. """ return self.queue_entries.filter(status='WAITING').count() @property def estimated_wait_time_minutes(self): """ Calculate estimated wait time. """ queue_size = self.current_queue_size return queue_size * self.average_service_time_minutes def get_next_patient(self): """ Get the next patient in queue based on priority and wait time. Returns: QueueEntry: Next queue entry to be served, or None if queue is empty """ return self.queue_entries.filter( status='WAITING' ).order_by('priority_score', 'joined_at').first() def calculate_wait_time(self, position=None): """ Calculate estimated wait time for a given position in queue. Args: position: Queue position (defaults to current queue size + 1) Returns: int: Estimated wait time in minutes """ if position is None: position = self.current_queue_size + 1 # Calculate based on average service time and queue position estimated_minutes = (position - 1) * self.average_service_time_minutes # Add buffer for high-priority patients high_priority_count = self.queue_entries.filter( status='WAITING', priority_score__gte=5.0 ).count() if high_priority_count > 0: estimated_minutes += high_priority_count * 5 # 5 min buffer per high-priority patient return max(0, estimated_minutes) class QueueEntry(models.Model): """ Individual entry in a waiting queue. """ class QueueStatus(models.TextChoices): WAITING = 'WAITING', 'Waiting' CALLED = 'CALLED', 'Called' IN_SERVICE = 'IN_SERVICE', 'In Service' COMPLETED = 'COMPLETED', 'Completed' LEFT = 'LEFT', 'Left Queue' NO_SHOW = 'NO_SHOW', 'No Show' # Queue and Patient queue = models.ForeignKey( WaitingQueue, on_delete=models.CASCADE, related_name='queue_entries', help_text='Waiting queue' ) patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='queue_entries', help_text='Patient in queue' ) appointment = models.ForeignKey( AppointmentRequest, on_delete=models.CASCADE, related_name='queue_entries', help_text='Associated appointment' ) # Entry Information entry_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique entry identifier' ) # Queue Position and Priority queue_position = models.PositiveIntegerField( help_text='Position in queue' ) priority_score = models.FloatField( default=1.0, help_text='Priority score for queue ordering' ) # Timing Information joined_at = models.DateTimeField( auto_now_add=True, help_text='Time patient joined queue' ) estimated_service_time = models.DateTimeField( blank=True, null=True, help_text='Estimated service time' ) called_at = models.DateTimeField( blank=True, null=True, help_text='Time patient was called' ) served_at = models.DateTimeField( blank=True, null=True, help_text='Time patient was served' ) # Status status = models.CharField( max_length=20, choices=QueueStatus.choices, default=QueueStatus.WAITING, help_text='Queue entry status' ) # Provider Assignment assigned_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_queue_entries', help_text='Assigned provider' ) # Communication notification_sent = models.BooleanField( default=False, help_text='Notification sent to patient' ) notification_method = models.CharField( max_length=20, choices=[ ('SMS', 'SMS'), ('EMAIL', 'Email'), ('PHONE', 'Phone Call'), ('PAGER', 'Pager'), ('APP', 'Mobile App'), ], blank=True, null=True, help_text='Notification method used' ) # Notes notes = models.TextField( blank=True, null=True, help_text='Additional notes' ) # Metadata updated_at = models.DateTimeField(auto_now=True) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_queue_entries', help_text='User who last updated entry' ) class Meta: db_table = 'appointments_queue_entry' verbose_name = 'Queue Entry' verbose_name_plural = 'Queue Entries' ordering = ['queue', 'priority_score', 'joined_at'] indexes = [ models.Index(fields=['queue', 'status']), models.Index(fields=['patient']), models.Index(fields=['priority_score']), models.Index(fields=['joined_at']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.queue.name} (#{self.queue_position})" @property def wait_time_minutes(self): """ Calculate current wait time. """ if self.status == 'WAITING': return int((timezone.now() - self.joined_at).total_seconds() / 60) elif self.served_at: return int((self.served_at - self.joined_at).total_seconds() / 60) return None def mark_as_called(self): """ Mark queue entry as called. Returns: bool: True if successful, False otherwise """ if self.status != 'WAITING': return False self.status = 'CALLED' self.called_at = timezone.now() self.save(update_fields=['status', 'called_at', 'updated_at']) return True def mark_as_in_progress(self): """ Mark queue entry as in progress/being served. Returns: bool: True if successful, False otherwise """ if self.status not in ['WAITING', 'CALLED']: return False self.status = 'IN_SERVICE' self.served_at = timezone.now() if not self.called_at: self.called_at = self.served_at self.save(update_fields=['status', 'served_at', 'called_at', 'updated_at']) return True def mark_as_completed(self): """ Mark queue entry as completed. Returns: bool: True if successful, False otherwise """ if self.status != 'IN_SERVICE': return False self.status = 'COMPLETED' if not self.served_at: self.served_at = timezone.now() self.save(update_fields=['status', 'served_at', 'updated_at']) return True def mark_as_no_show(self): """ Mark queue entry as no show. Returns: bool: True if successful, False otherwise """ if self.status not in ['WAITING', 'CALLED']: return False self.status = 'NO_SHOW' self.save(update_fields=['status', 'updated_at']) return True def mark_as_cancelled(self): """ Mark queue entry as cancelled/left queue. Returns: bool: True if successful, False otherwise """ if self.status in ['COMPLETED', 'NO_SHOW']: return False self.status = 'LEFT' self.save(update_fields=['status', 'updated_at']) return True def mark_as_removed(self): """ Remove entry from queue (alias for mark_as_cancelled). Returns: bool: True if successful, False otherwise """ return self.mark_as_cancelled() class TelemedicineSession(models.Model): """ Telemedicine session management. """ class Platform(models.TextChoices): ZOOM = 'ZOOM', 'Zoom' TEAMS = 'TEAMS', 'Microsoft Teams' WEBEX = 'WEBEX', 'Cisco Webex' DOXY = 'DOXY', 'Doxy.me' CUSTOM = 'CUSTOM', 'Custom Platform' OTHER = 'OTHER', 'Other' class SessionStatus(models.TextChoices): SCHEDULED = 'SCHEDULED', 'Scheduled' READY = 'READY', 'Ready to Start' WAITING = 'WAITING', 'Waiting' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' FAILED = 'FAILED', 'Failed' # Session Information session_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique session identifier' ) # Associated Appointment appointment = models.OneToOneField( AppointmentRequest, on_delete=models.CASCADE, related_name='telemedicine_session', help_text='Associated appointment' ) # Platform Information platform = models.CharField( max_length=50, choices=Platform.choices, help_text='Telemedicine platform' ) # Meeting Details meeting_url = models.URLField( help_text='Meeting URL' ) meeting_id = models.CharField( max_length=100, help_text='Meeting ID or room number' ) meeting_password = models.CharField( max_length=100, blank=True, null=True, help_text='Meeting password' ) # Session Configuration waiting_room_enabled = models.BooleanField( default=True, help_text='Waiting room enabled' ) recording_enabled = models.BooleanField( default=False, help_text='Session recording enabled' ) recording_consent = models.BooleanField( default=False, help_text='Patient consent for recording' ) # Security Settings encryption_enabled = models.BooleanField( default=True, help_text='End-to-end encryption enabled' ) password_required = models.BooleanField( default=True, help_text='Password required to join' ) # Session Status status = models.CharField( max_length=20, choices=SessionStatus.choices, default=SessionStatus.SCHEDULED, help_text='Session status' ) # Timing Information scheduled_start = models.DateTimeField( help_text='Scheduled start time' ) scheduled_end = models.DateTimeField( help_text='Scheduled end time' ) actual_start = models.DateTimeField( blank=True, null=True, help_text='Actual start time' ) actual_end = models.DateTimeField( blank=True, null=True, help_text='Actual end time' ) # Participants provider_joined_at = models.DateTimeField( blank=True, null=True, help_text='Provider join time' ) patient_joined_at = models.DateTimeField( blank=True, null=True, help_text='Patient join time' ) # Technical Information connection_quality = models.CharField( max_length=20, choices=[ ('EXCELLENT', 'Excellent'), ('GOOD', 'Good'), ('FAIR', 'Fair'), ('POOR', 'Poor'), ], blank=True, null=True, help_text='Connection quality' ) technical_issues = models.TextField( blank=True, null=True, help_text='Technical issues encountered' ) # Recording Information recording_url = models.URLField( blank=True, null=True, help_text='Recording URL' ) recording_duration_minutes = models.PositiveIntegerField( blank=True, null=True, help_text='Recording duration in minutes' ) # Session Notes session_notes = models.TextField( blank=True, null=True, help_text='Session notes' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_telemedicine_sessions', help_text='User who created the session' ) class Meta: db_table = 'appointments_telemedicine_session' verbose_name = 'Telemedicine Session' verbose_name_plural = 'Telemedicine Sessions' ordering = ['-scheduled_start'] indexes = [ models.Index(fields=['appointment']), models.Index(fields=['status']), models.Index(fields=['scheduled_start']), ] def __str__(self): return f"Telemedicine - {self.appointment.patient.get_full_name()} ({self.status})" @property def duration_minutes(self): """ Calculate session duration. """ if self.actual_start and self.actual_end: return int((self.actual_end - self.actual_start).total_seconds() / 60) return None def mark_provider_joined(self): """ Mark provider as joined to the telemedicine session. Returns: bool: True if successful, False otherwise """ if self.status not in ['SCHEDULED', 'READY', 'WAITING']: return False self.provider_joined_at = timezone.now() # If patient already joined, start the session if self.patient_joined_at and self.status != 'IN_PROGRESS': self.status = 'IN_PROGRESS' self.actual_start = timezone.now() elif self.status == 'SCHEDULED': self.status = 'WAITING' self.save(update_fields=['provider_joined_at', 'status', 'actual_start', 'updated_at']) return True def mark_patient_joined(self): """ Mark patient as joined to the telemedicine session. Returns: bool: True if successful, False otherwise """ if self.status not in ['SCHEDULED', 'READY', 'WAITING']: return False self.patient_joined_at = timezone.now() # If provider already joined, start the session if self.provider_joined_at and self.status != 'IN_PROGRESS': self.status = 'IN_PROGRESS' self.actual_start = timezone.now() elif self.status == 'SCHEDULED': self.status = 'WAITING' self.save(update_fields=['patient_joined_at', 'status', 'actual_start', 'updated_at']) return True def end_session(self): """ End the telemedicine session. Returns: bool: True if successful, False otherwise """ if self.status != 'IN_PROGRESS': return False self.status = 'COMPLETED' self.actual_end = timezone.now() # Update associated appointment if self.appointment: self.appointment.status = 'COMPLETED' self.appointment.completed_at = self.actual_end self.appointment.save(update_fields=['status', 'completed_at', 'updated_at']) self.save(update_fields=['status', 'actual_end', 'updated_at']) return True def cancel_session(self): """ Cancel the telemedicine session. Returns: bool: True if successful, False otherwise """ if self.status in ['COMPLETED', 'CANCELLED']: return False self.status = 'CANCELLED' # Update associated appointment if self.appointment and self.appointment.status != 'CANCELLED': self.appointment.status = 'CANCELLED' self.appointment.cancelled_at = timezone.now() self.appointment.save(update_fields=['status', 'cancelled_at', 'updated_at']) self.save(update_fields=['status', 'updated_at']) return True class AppointmentTemplate(models.Model): """ Templates for common appointment types. """ # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='appointment_templates', help_text='Organization tenant' ) # Template Information name = models.CharField( max_length=200, help_text='Template name' ) description = models.TextField( blank=True, null=True, help_text='Template description' ) # Appointment Configuration appointment_type = models.CharField( max_length=50, help_text='Default appointment type' ) specialty = models.CharField( max_length=100, help_text='Medical specialty' ) duration_minutes = models.PositiveIntegerField( help_text='Default duration in minutes' ) # Scheduling Rules advance_booking_days = models.PositiveIntegerField( default=30, help_text='Maximum advance booking days' ) minimum_notice_hours = models.PositiveIntegerField( default=24, help_text='Minimum notice required in hours' ) # Requirements insurance_verification_required = models.BooleanField( default=False, help_text='Insurance verification required' ) authorization_required = models.BooleanField( default=False, help_text='Prior authorization required' ) # Instructions pre_appointment_instructions = models.TextField( blank=True, null=True, help_text='Pre-appointment instructions for patient' ) post_appointment_instructions = models.TextField( blank=True, null=True, help_text='Post-appointment instructions template' ) # Forms and Documents required_forms = models.JSONField( default=list, blank=True, help_text='Required forms for this appointment type' ) # Status is_active = models.BooleanField( default=True, help_text='Template is active' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_appointment_templates', help_text='User who created the template' ) class Meta: db_table = 'appointments_appointment_template' verbose_name = 'Appointment Template' verbose_name_plural = 'Appointment Templates' ordering = ['specialty', 'name'] indexes = [ models.Index(fields=['tenant', 'specialty']), models.Index(fields=['appointment_type']), models.Index(fields=['is_active']), ] def __str__(self): return f"{self.name} ({self.specialty})" class WaitingList(models.Model): """ Patient waiting list for appointment scheduling. Follows healthcare industry standards for patient queue management. """ class AppointmentType(models.TextChoices): CONSULTATION = 'CONSULTATION', 'Consultation' FOLLOW_UP = 'FOLLOW_UP', 'Follow-up' PROCEDURE = 'PROCEDURE', 'Procedure' SURGERY = 'SURGERY', 'Surgery' DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic' THERAPY = 'THERAPY', 'Therapy' VACCINATION = 'VACCINATION', 'Vaccination' SCREENING = 'SCREENING', 'Screening' EMERGENCY = 'EMERGENCY', 'Emergency' TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine' OTHER = 'OTHER', 'Other' class Specialty(models.TextChoices): FAMILY_MEDICINE = 'FAMILY_MEDICINE', 'Family Medicine' INTERNAL_MEDICINE = 'INTERNAL_MEDICINE', 'Internal Medicine' PEDIATRICS = 'PEDIATRICS', 'Pediatrics' CARDIOLOGY = 'CARDIOLOGY', 'Cardiology' DERMATOLOGY = 'DERMATOLOGY', 'Dermatology' ENDOCRINOLOGY = 'ENDOCRINOLOGY', 'Endocrinology' GASTROENTEROLOGY = 'GASTROENTEROLOGY', 'Gastroenterology' NEUROLOGY = 'NEUROLOGY', 'Neurology' ONCOLOGY = 'ONCOLOGY', 'Oncology' ORTHOPEDICS = 'ORTHOPEDICS', 'Orthopedics' PSYCHIATRY = 'PSYCHIATRY', 'Psychiatry' RADIOLOGY = 'RADIOLOGY', 'Radiology' SURGERY = 'SURGERY', 'Surgery' UROLOGY = 'UROLOGY', 'Urology' GYNECOLOGY = 'GYNECOLOGY', 'Gynecology' OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology' ENT = 'ENT', 'Ear, Nose & Throat' EMERGENCY = 'EMERGENCY', 'Emergency Medicine' OTHER = 'OTHER', 'Other' class Priority(models.TextChoices): ROUTINE = 'ROUTINE', 'Routine' URGENT = 'URGENT', 'Urgent' STAT = 'STAT', 'STAT' EMERGENCY = 'EMERGENCY', 'Emergency' class ContactMethod(models.TextChoices): PHONE = 'PHONE', 'Phone' EMAIL = 'EMAIL', 'Email' SMS = 'SMS', 'SMS' PORTAL = 'PORTAL', 'Patient Portal' MAIL = 'MAIL', 'Mail' class RequestStatus(models.TextChoices): ACTIVE = 'ACTIVE', 'Active' CONTACTED = 'CONTACTED', 'Contacted' OFFERED = 'OFFERED', 'Appointment Offered' SCHEDULED = 'SCHEDULED', 'Scheduled' CANCELLED = 'CANCELLED', 'Cancelled' EXPIRED = 'EXPIRED', 'Expired' TRANSFERRED = 'TRANSFERRED', 'Transferred' class AuthorizationStatus(models.TextChoices): NOT_REQUIRED = 'NOT_REQUIRED', 'Not Required' PENDING = 'PENDING', 'Pending' APPROVED = 'APPROVED', 'Approved' DENIED = 'DENIED', 'Denied' EXPIRED = 'EXPIRED', 'Expired' class ReferralUrgency(models.TextChoices): ROUTINE = 'ROUTINE', 'Routine' URGENT = 'URGENT', 'Urgent' STAT = 'STAT', 'STAT' class RemovalReason(models.TextChoices): SCHEDULED = 'SCHEDULED', 'Appointment Scheduled' PATIENT_CANCELLED = 'PATIENT_CANCELLED', 'Patient Cancelled' PROVIDER_CANCELLED = 'PROVIDER_CANCELLED', 'Provider Cancelled' NO_RESPONSE = 'NO_RESPONSE', 'No Response to Contact' INSURANCE_ISSUE = 'INSURANCE_ISSUE', 'Insurance Issue' TRANSFERRED = 'TRANSFERRED', 'Transferred to Another Provider' EXPIRED = 'EXPIRED', 'Entry Expired' DUPLICATE = 'DUPLICATE', 'Duplicate Entry' OTHER = 'OTHER', 'Other' # Basic Identifiers waiting_list_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique waiting list entry identifier' ) # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='waiting_list_entries', help_text='Organization tenant' ) # Patient Information patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='waiting_list_entries', help_text='Patient on waiting list' ) # Provider and Service Information provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='provider_waiting_list', blank=True, null=True, help_text='Preferred healthcare provider' ) department = models.ForeignKey( 'hr.Department', on_delete=models.CASCADE, related_name='waiting_list_entries', help_text='Department for appointment' ) appointment_type = models.CharField( max_length=50, choices=AppointmentType.choices, help_text='Type of appointment requested' ) specialty = models.CharField( max_length=100, choices=Specialty.choices, help_text='Medical specialty required' ) # Priority and Clinical Information priority = models.CharField( max_length=20, choices=Priority.choices, default=Priority.ROUTINE, help_text='Clinical priority level' ) urgency_score = models.PositiveIntegerField( default=1, validators=[MinValueValidator(1), MaxValueValidator(10)], help_text='Clinical urgency score (1-10, 10 being most urgent)' ) clinical_indication = models.TextField( help_text='Clinical reason for appointment request' ) diagnosis_codes = models.JSONField( default=list, blank=True, help_text='ICD-10 diagnosis codes' ) # Patient Preferences preferred_date = models.DateField( blank=True, null=True, help_text='Patient preferred appointment date' ) preferred_time = models.TimeField( blank=True, null=True, help_text='Patient preferred appointment time' ) flexible_scheduling = models.BooleanField( default=True, help_text='Patient accepts alternative dates/times' ) earliest_acceptable_date = models.DateField( blank=True, null=True, help_text='Earliest acceptable appointment date' ) latest_acceptable_date = models.DateField( blank=True, null=True, help_text='Latest acceptable appointment date' ) acceptable_days = models.JSONField( default=list, null=True, blank=True, help_text='Acceptable days of week (0=Monday, 6=Sunday)' ) acceptable_times = models.JSONField( default=list, blank=True, help_text='Acceptable time ranges' ) # Communication Preferences contact_method = models.CharField( max_length=20, choices=ContactMethod.choices, default=ContactMethod.PHONE, help_text='Preferred contact method' ) contact_phone = models.CharField( max_length=20, blank=True, null=True, help_text='Contact phone number' ) contact_email = models.EmailField( blank=True, null=True, help_text='Contact email address' ) # Status and Workflow status = models.CharField( max_length=20, choices=RequestStatus.choices, default=RequestStatus.ACTIVE, help_text='Waiting list status' ) # Position and Timing position = models.PositiveIntegerField( blank=True, null=True, help_text='Position in waiting list queue' ) estimated_wait_time = models.PositiveIntegerField( blank=True, null=True, help_text='Estimated wait time in days' ) # Contact History last_contacted = models.DateTimeField( blank=True, null=True, help_text='Last contact attempt date/time' ) contact_attempts = models.PositiveIntegerField( default=0, help_text='Number of contact attempts made' ) max_contact_attempts = models.PositiveIntegerField( default=3, help_text='Maximum contact attempts before expiring' ) # Appointment Offers appointments_offered = models.PositiveIntegerField( default=0, help_text='Number of appointments offered' ) appointments_declined = models.PositiveIntegerField( default=0, help_text='Number of appointments declined' ) last_offer_date = models.DateTimeField( blank=True, null=True, help_text='Date of last appointment offer' ) # Scheduling Constraints requires_interpreter = models.BooleanField( default=False, help_text='Patient requires interpreter services' ) interpreter_language = models.CharField( max_length=50, blank=True, null=True, help_text='Required interpreter language' ) accessibility_requirements = models.TextField( blank=True, null=True, help_text='Special accessibility requirements' ) transportation_needed = models.BooleanField( default=False, help_text='Patient needs transportation assistance' ) # Insurance and Authorization insurance_verified = models.BooleanField( default=False, help_text='Insurance coverage verified' ) authorization_required = models.BooleanField( default=False, help_text='Prior authorization required' ) authorization_status = models.CharField( max_length=20, choices=AuthorizationStatus.choices, default=AuthorizationStatus.NOT_REQUIRED, help_text='Authorization status' ) authorization_number = models.CharField( max_length=100, blank=True, null=True, help_text='Authorization number' ) # Referral Information referring_provider = models.CharField( max_length=200, blank=True, null=True, help_text='Referring provider name' ) referral_date = models.DateField( blank=True, null=True, help_text='Date of referral' ) referral_urgency = models.CharField( max_length=20, choices=ReferralUrgency.choices, default=ReferralUrgency.ROUTINE, help_text='Referral urgency level' ) # Outcome Tracking scheduled_appointment = models.ForeignKey( 'AppointmentRequest', on_delete=models.SET_NULL, blank=True, null=True, related_name='waiting_list_entry', help_text='Scheduled appointment from waiting list' ) removal_reason = models.CharField( max_length=50, choices=RemovalReason.choices, blank=True, null=True, help_text='Reason for removal from waiting list' ) removal_notes = models.TextField( blank=True, null=True, help_text='Additional notes about removal' ) removed_at = models.DateTimeField( blank=True, null=True, help_text='Date/time removed from waiting list' ) removed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, related_name='removed_waiting_list_entries', help_text='User who removed entry from waiting list' ) # Audit Trail created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_waiting_list_entries', help_text='User who created the waiting list entry' ) # Notes and Comments notes = models.TextField( blank=True, null=True, help_text='Additional notes and comments' ) class Meta: db_table = 'appointments_waiting_list' verbose_name = 'Waiting List Entry' verbose_name_plural = 'Waiting List Entries' ordering = ['priority', 'urgency_score', 'created_at'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'status']), models.Index(fields=['department', 'specialty', 'status']), models.Index(fields=['priority', 'urgency_score']), models.Index(fields=['status', 'created_at']), models.Index(fields=['provider', 'status']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.specialty} ({self.status})" @property def days_waiting(self): """Calculate number of days patient has been waiting.""" return (timezone.now().date() - self.created_at.date()).days @property def is_overdue_contact(self): """Check if contact is overdue based on priority.""" if not self.last_contacted: return self.days_waiting > 1 days_since_contact = (timezone.now().date() - self.last_contacted.date()).days if self.priority == 'EMERGENCY': return days_since_contact > 0 # Same day contact required elif self.priority == 'STAT': return days_since_contact > 1 # Next day contact required elif self.priority == 'URGENT': return days_since_contact > 3 # 3 day contact window else: return days_since_contact > 7 # Weekly contact for routine @property def should_expire(self): """Check if waiting list entry should expire.""" if self.contact_attempts >= self.max_contact_attempts: return True # Expire after 90 days for routine, 30 days for urgent max_days = 30 if self.priority in ['URGENT', 'STAT', 'EMERGENCY'] else 90 return self.days_waiting > max_days def calculate_position(self): """Calculate position in waiting list queue.""" # Priority-based position calculation priority_weights = { 'EMERGENCY': 1000, 'STAT': 800, 'URGENT': 600, 'ROUTINE': 400, } base_score = priority_weights.get(self.priority, 400) urgency_bonus = self.urgency_score * 10 wait_time_bonus = min(self.days_waiting, 30) # Cap at 30 days total_score = base_score + urgency_bonus + wait_time_bonus # Count entries with higher scores higher_priority = WaitingList.objects.filter( department=self.department, specialty=self.specialty, status='ACTIVE', tenant=self.tenant ).exclude(id=self.id) position = 1 for entry in higher_priority: entry_score = ( priority_weights.get(entry.priority, 400) + entry.urgency_score * 10 + min(entry.days_waiting, 30) ) if entry_score > total_score: position += 1 return position def update_position(self): """Update position in waiting list.""" self.position = self.calculate_position() self.save(update_fields=['position']) def estimate_wait_time(self): """Estimate wait time based on historical data and current queue.""" # This would typically use historical scheduling data # For now, provide basic estimation base_wait = { 'EMERGENCY': 1, 'STAT': 3, 'URGENT': 7, 'ROUTINE': 14, } estimated_days = base_wait.get(self.priority, 14) # Adjust based on queue position if self.position: estimated_days += max(0, (self.position - 1) * 2) return estimated_days class WaitingListContactLog(models.Model): """ Contact log for waiting list entries. Tracks all communication attempts with patients on waiting list. """ class ContactMethod(models.TextChoices): PHONE = 'PHONE', 'Phone Call' EMAIL = 'EMAIL', 'Email' SMS = 'SMS', 'SMS' PORTAL = 'PORTAL', 'Patient Portal Message' MAIL = 'MAIL', 'Mail' IN_PERSON = 'IN_PERSON', 'In Person' class ContactOutcome(models.TextChoices): SUCCESSFUL = 'SUCCESSFUL', 'Successful Contact' NO_ANSWER = 'NO_ANSWER', 'No Answer' BUSY = 'BUSY', 'Line Busy' VOICEMAIL = 'VOICEMAIL', 'Left Voicemail' EMAIL_SENT = 'EMAIL_SENT', 'Email Sent' EMAIL_BOUNCED = 'EMAIL_BOUNCED', 'Email Bounced' SMS_SENT = 'SMS_SENT', 'SMS Sent' SMS_FAILED = 'SMS_FAILED', 'SMS Failed' WRONG_NUMBER = 'WRONG_NUMBER', 'Wrong Number' DECLINED = 'DECLINED', 'Patient Declined' class PatientResponse(models.TextChoices): ACCEPTED = 'ACCEPTED', 'Accepted Appointment' DECLINED = 'DECLINED', 'Declined Appointment' REQUESTED_DIFFERENT = 'REQUESTED_DIFFERENT', 'Requested Different Time' WILL_CALL_BACK = 'WILL_CALL_BACK', 'Will Call Back' NO_LONGER_NEEDED = 'NO_LONGER_NEEDED', 'No Longer Needed' INSURANCE_ISSUE = 'INSURANCE_ISSUE', 'Insurance Issue' NO_RESPONSE = 'NO_RESPONSE', 'No Response' waiting_list_entry = models.ForeignKey( WaitingList, on_delete=models.CASCADE, related_name='contact_logs', help_text='Associated waiting list entry' ) contact_date = models.DateTimeField( auto_now_add=True, help_text='Date and time of contact attempt' ) contact_method = models.CharField( max_length=20, choices=ContactMethod.choices, help_text='Method of contact used' ) contact_outcome = models.CharField( max_length=20, choices=ContactOutcome.choices, help_text='Outcome of contact attempt' ) appointment_offered = models.BooleanField( default=False, help_text='Appointment was offered during contact' ) offered_date = models.DateField( blank=True, null=True, help_text='Date of offered appointment' ) offered_time = models.TimeField( blank=True, null=True, help_text='Time of offered appointment' ) patient_response = models.CharField( max_length=20, choices=PatientResponse.choices, blank=True, null=True, help_text='Patient response to contact' ) notes = models.TextField( blank=True, null=True, help_text='Notes from contact attempt' ) next_contact_date = models.DateField( blank=True, null=True, help_text='Scheduled date for next contact attempt' ) contacted_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, help_text='Staff member who made contact' ) class Meta: db_table = 'appointments_waiting_list_contact_log' verbose_name = 'Waiting List Contact Log' verbose_name_plural = 'Waiting List Contact Logs' ordering = ['-contact_date'] indexes = [ models.Index(fields=['waiting_list_entry', 'contact_date']), models.Index(fields=['contact_outcome']), models.Index(fields=['next_contact_date']), ] def __str__(self): return f"{self.waiting_list_entry.patient.get_full_name()} - {self.contact_method} ({self.contact_outcome})" class SchedulingPreference(models.Model): """ Store patient scheduling preferences and behavioral patterns. Used by SmartScheduler for intelligent slot recommendations. """ # Tenant and Patient tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='scheduling_preferences', help_text='Organization tenant' ) patient = models.OneToOneField( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='scheduling_preference', help_text='Patient profile' ) # Preference Data preferred_days = models.JSONField( default=list, blank=True, help_text='Preferred days of week (e.g., ["Monday", "Wednesday"])' ) preferred_times = models.JSONField( default=list, blank=True, help_text='Preferred times of day (e.g., ["morning", "afternoon"])' ) preferred_providers = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='preferred_by_patients', blank=True, help_text='Preferred healthcare providers' ) # Behavioral Data average_no_show_rate = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Average no-show rate percentage' ) average_late_arrival_minutes = models.IntegerField( default=0, help_text='Average late arrival in minutes' ) total_appointments = models.IntegerField( default=0, help_text='Total number of appointments' ) completed_appointments = models.IntegerField( default=0, help_text='Number of completed appointments' ) # Geographic Data home_address = models.TextField( blank=True, null=True, help_text='Patient home address' ) travel_time_to_clinic = models.DurationField( blank=True, null=True, help_text='Estimated travel time to clinic' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'appointments_scheduling_preference' verbose_name = 'Scheduling Preference' verbose_name_plural = 'Scheduling Preferences' indexes = [ models.Index(fields=['tenant', 'patient']), models.Index(fields=['average_no_show_rate']), ] def __str__(self): return f"Preferences for {self.patient.get_full_name()}" @property def completion_rate(self): """Calculate appointment completion rate.""" if self.total_appointments == 0: return 0 return round((self.completed_appointments / self.total_appointments) * 100, 2) class AppointmentPriorityRule(models.Model): """ Define rules for appointment prioritization. Used by SmartScheduler for priority routing. """ # Tenant tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='appointment_priority_rules', help_text='Organization tenant' ) # Rule Information name = models.CharField( max_length=100, help_text='Rule name' ) description = models.TextField( help_text='Rule description' ) # Rule Conditions appointment_types = models.ManyToManyField( 'AppointmentTemplate', related_name='priority_rules', blank=True, help_text='Applicable appointment types' ) specialties = models.JSONField( default=list, blank=True, help_text='Applicable specialties' ) diagnosis_codes = models.JSONField( default=list, blank=True, help_text='ICD-10 diagnosis codes that trigger this rule' ) # Priority Scoring base_priority_score = models.IntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Base priority score (0-100)' ) urgency_multiplier = models.DecimalField( max_digits=3, decimal_places=2, default=1.0, validators=[MinValueValidator(0.1), MaxValueValidator(10.0)], help_text='Urgency multiplier for scoring' ) # Time Constraints max_wait_days = models.IntegerField( blank=True, null=True, help_text='Maximum acceptable wait time in days' ) requires_same_day = models.BooleanField( default=False, help_text='Requires same-day appointment' ) # Status is_active = models.BooleanField( default=True, help_text='Rule is active' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_priority_rules', help_text='User who created the rule' ) class Meta: db_table = 'appointments_priority_rule' verbose_name = 'Appointment Priority Rule' verbose_name_plural = 'Appointment Priority Rules' ordering = ['-base_priority_score', 'name'] indexes = [ models.Index(fields=['tenant', 'is_active']), models.Index(fields=['base_priority_score']), ] def __str__(self): return f"{self.name} (Score: {self.base_priority_score})" class SchedulingMetrics(models.Model): """ Track scheduling performance metrics for providers. Used for analytics and optimization. """ # Tenant and Provider tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='scheduling_metrics', help_text='Organization tenant' ) provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='scheduling_metrics', help_text='Healthcare provider' ) date = models.DateField( help_text='Metrics date' ) # Utilization Metrics total_slots = models.IntegerField( default=0, help_text='Total available slots' ) booked_slots = models.IntegerField( default=0, help_text='Number of booked slots' ) completed_appointments = models.IntegerField( default=0, help_text='Number of completed appointments' ) no_shows = models.IntegerField( default=0, help_text='Number of no-shows' ) cancellations = models.IntegerField( default=0, help_text='Number of cancellations' ) # Time Metrics average_appointment_duration = models.DurationField( blank=True, null=True, help_text='Average appointment duration' ) average_wait_time = models.DurationField( blank=True, null=True, help_text='Average patient wait time' ) total_overtime_minutes = models.IntegerField( default=0, help_text='Total overtime in minutes' ) # Calculated Fields utilization_rate = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Slot utilization rate percentage' ) no_show_rate = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='No-show rate percentage' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'appointments_scheduling_metrics' verbose_name = 'Scheduling Metrics' verbose_name_plural = 'Scheduling Metrics' unique_together = ['tenant', 'provider', 'date'] ordering = ['-date'] indexes = [ models.Index(fields=['tenant', 'provider', 'date']), models.Index(fields=['date']), models.Index(fields=['utilization_rate']), models.Index(fields=['no_show_rate']), ] def __str__(self): return f"{self.provider.get_full_name()} - {self.date}" def calculate_utilization_rate(self): """Calculate and update utilization rate.""" if self.total_slots > 0: self.utilization_rate = round((self.booked_slots / self.total_slots) * 100, 2) else: self.utilization_rate = 0 return self.utilization_rate def calculate_no_show_rate(self): """Calculate and update no-show rate.""" if self.booked_slots > 0: self.no_show_rate = round((self.no_shows / self.booked_slots) * 100, 2) else: self.no_show_rate = 0 return self.no_show_rate def save(self, *args, **kwargs): """Override save to auto-calculate rates.""" self.calculate_utilization_rate() self.calculate_no_show_rate() super().save(*args, **kwargs) class QueueConfiguration(models.Model): """ Advanced queue configuration and rules for dynamic positioning. Extends WaitingQueue with intelligent queue management settings. """ # One-to-one relationship with WaitingQueue queue = models.OneToOneField( WaitingQueue, on_delete=models.CASCADE, related_name='configuration', help_text='Associated waiting queue' ) # Dynamic Positioning Rules use_dynamic_positioning = models.BooleanField( default=True, help_text='Enable AI-powered dynamic queue positioning' ) priority_weight = models.DecimalField( max_digits=3, decimal_places=2, default=0.50, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='Weight for priority score factor (0.0-1.0)' ) wait_time_weight = models.DecimalField( max_digits=3, decimal_places=2, default=0.30, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='Weight for wait time fairness factor (0.0-1.0)' ) appointment_time_weight = models.DecimalField( max_digits=3, decimal_places=2, default=0.20, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='Weight for appointment time proximity factor (0.0-1.0)' ) # Capacity Management enable_overflow_queue = models.BooleanField( default=False, help_text='Enable overflow queue when capacity is reached' ) overflow_threshold = models.IntegerField( default=10, validators=[MinValueValidator(1)], help_text='Queue size threshold to trigger overflow' ) # Wait Time Estimation use_historical_data = models.BooleanField( default=True, help_text='Use historical data for wait time estimation' ) default_service_time_minutes = models.IntegerField( default=20, validators=[MinValueValidator(5), MaxValueValidator(480)], help_text='Default service time in minutes' ) historical_data_days = models.IntegerField( default=7, validators=[MinValueValidator(1), MaxValueValidator(90)], help_text='Number of days of historical data to use' ) # Real-time Updates enable_websocket_updates = models.BooleanField( default=True, help_text='Enable WebSocket real-time updates' ) update_interval_seconds = models.IntegerField( default=30, validators=[MinValueValidator(5), MaxValueValidator(300)], help_text='Update broadcast interval in seconds' ) # Load Factor Configuration load_factor_normal_threshold = models.DecimalField( max_digits=3, decimal_places=2, default=0.50, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='Utilization threshold for normal load (0.0-1.0)' ) load_factor_moderate_threshold = models.DecimalField( max_digits=3, decimal_places=2, default=0.75, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='Utilization threshold for moderate load (0.0-1.0)' ) load_factor_high_threshold = models.DecimalField( max_digits=3, decimal_places=2, default=0.90, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='Utilization threshold for high load (0.0-1.0)' ) # Auto-repositioning Settings auto_reposition_enabled = models.BooleanField( default=True, help_text='Automatically reposition queue entries' ) reposition_interval_minutes = models.IntegerField( default=15, validators=[MinValueValidator(1), MaxValueValidator(120)], help_text='Interval for auto-repositioning in minutes' ) # Notification Settings notify_on_position_change = models.BooleanField( default=True, help_text='Notify patients when queue position changes significantly' ) position_change_threshold = models.IntegerField( default=3, validators=[MinValueValidator(1)], help_text='Position change threshold to trigger notification' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'appointments_queue_configuration' verbose_name = 'Queue Configuration' verbose_name_plural = 'Queue Configurations' def __str__(self): return f"Configuration for {self.queue.name}" def validate_weights(self): """Validate that weights sum to approximately 1.0.""" total = float(self.priority_weight + self.wait_time_weight + self.appointment_time_weight) if not (0.95 <= total <= 1.05): # Allow small floating point variance from django.core.exceptions import ValidationError raise ValidationError( f'Weights must sum to 1.0 (current sum: {total:.2f})' ) def save(self, *args, **kwargs): """Override save to validate weights.""" self.validate_weights() super().save(*args, **kwargs) class QueueMetrics(models.Model): """ Track queue performance metrics for analytics and optimization. Stores hourly granular data for trend analysis. """ # Queue and Time Period queue = models.ForeignKey( WaitingQueue, on_delete=models.CASCADE, related_name='metrics', help_text='Associated waiting queue' ) date = models.DateField( help_text='Metrics date' ) hour = models.IntegerField( validators=[MinValueValidator(0), MaxValueValidator(23)], help_text='Hour of day (0-23)' ) # Volume Metrics total_entries = models.IntegerField( default=0, help_text='Total queue entries during this hour' ) completed_entries = models.IntegerField( default=0, help_text='Number of completed entries' ) no_shows = models.IntegerField( default=0, help_text='Number of no-shows' ) left_queue = models.IntegerField( default=0, help_text='Number of patients who left queue' ) # Time Metrics average_wait_time_minutes = models.DecimalField( max_digits=6, decimal_places=2, default=0, help_text='Average wait time in minutes' ) max_wait_time_minutes = models.IntegerField( default=0, help_text='Maximum wait time in minutes' ) min_wait_time_minutes = models.IntegerField( default=0, help_text='Minimum wait time in minutes' ) average_service_time_minutes = models.DecimalField( max_digits=6, decimal_places=2, default=0, help_text='Average service time in minutes' ) # Queue State Metrics peak_queue_size = models.IntegerField( default=0, help_text='Peak queue size during this hour' ) average_queue_size = models.DecimalField( max_digits=6, decimal_places=2, default=0, help_text='Average queue size during this hour' ) min_queue_size = models.IntegerField( default=0, help_text='Minimum queue size during this hour' ) # Efficiency Metrics throughput = models.IntegerField( default=0, help_text='Number of patients served per hour' ) utilization_rate = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Queue utilization rate percentage' ) # Quality Metrics no_show_rate = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='No-show rate percentage' ) abandonment_rate = models.DecimalField( max_digits=5, decimal_places=2, default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Queue abandonment rate percentage' ) # Dynamic Positioning Metrics repositioning_events = models.IntegerField( default=0, help_text='Number of queue repositioning events' ) average_position_changes = models.DecimalField( max_digits=5, decimal_places=2, default=0, help_text='Average position changes per entry' ) # Load Metrics average_load_factor = models.DecimalField( max_digits=3, decimal_places=2, default=1.0, help_text='Average load factor during this hour' ) peak_load_factor = models.DecimalField( max_digits=3, decimal_places=2, default=1.0, help_text='Peak load factor during this hour' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'appointments_queue_metrics' verbose_name = 'Queue Metrics' verbose_name_plural = 'Queue Metrics' unique_together = ['queue', 'date', 'hour'] ordering = ['-date', '-hour'] indexes = [ models.Index(fields=['queue', 'date']), models.Index(fields=['date', 'hour']), models.Index(fields=['queue', 'date', 'hour']), models.Index(fields=['utilization_rate']), models.Index(fields=['no_show_rate']), ] def __str__(self): return f"{self.queue.name} - {self.date} {self.hour:02d}:00" def calculate_no_show_rate(self): """Calculate and update no-show rate.""" if self.total_entries > 0: self.no_show_rate = round((self.no_shows / self.total_entries) * 100, 2) else: self.no_show_rate = 0 return self.no_show_rate def calculate_abandonment_rate(self): """Calculate and update abandonment rate.""" if self.total_entries > 0: self.abandonment_rate = round((self.left_queue / self.total_entries) * 100, 2) else: self.abandonment_rate = 0 return self.abandonment_rate def calculate_utilization_rate(self): """Calculate and update utilization rate.""" if self.peak_queue_size > 0: # Utilization based on how full the queue was capacity = self.queue.max_queue_size or 50 self.utilization_rate = round((self.peak_queue_size / capacity) * 100, 2) else: self.utilization_rate = 0 return self.utilization_rate def save(self, *args, **kwargs): """Override save to auto-calculate rates.""" self.calculate_no_show_rate() self.calculate_abandonment_rate() self.calculate_utilization_rate() super().save(*args, **kwargs)