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

2762 lines
81 KiB
Python

"""
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)