Marwan Alwali 2780a2dc7c update
2025-09-16 15:10:57 +03:00

1817 lines
50 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.
"""
APPOINTMENT_TYPE_CHOICES = [
('CONSULTATION', 'Consultation'),
('FOLLOW_UP', 'Follow-up'),
('PROCEDURE', 'Procedure'),
('SURGERY', 'Surgery'),
('DIAGNOSTIC', 'Diagnostic'),
('THERAPY', 'Therapy'),
('VACCINATION', 'Vaccination'),
('SCREENING', 'Screening'),
('EMERGENCY', 'Emergency'),
('TELEMEDICINE', 'Telemedicine'),
('OTHER', 'Other'),
]
SPECIALTY_CHOICES = [
('FAMILY_MEDICINE', 'Family Medicine'),
('INTERNAL_MEDICINE', 'Internal Medicine'),
('PEDIATRICS', 'Pediatrics'),
('CARDIOLOGY', 'Cardiology'),
('DERMATOLOGY', 'Dermatology'),
('ENDOCRINOLOGY', 'Endocrinology'),
('GASTROENTEROLOGY', 'Gastroenterology'),
('NEUROLOGY', 'Neurology'),
('ONCOLOGY', 'Oncology'),
('ORTHOPEDICS', 'Orthopedics'),
('PSYCHIATRY', 'Psychiatry'),
('RADIOLOGY', 'Radiology'),
('SURGERY', 'Surgery'),
('UROLOGY', 'Urology'),
('GYNECOLOGY', 'Gynecology'),
('OPHTHALMOLOGY', 'Ophthalmology'),
('ENT', 'Ear, Nose & Throat'),
('EMERGENCY', 'Emergency Medicine'),
('OTHER', 'Other'),
]
PRIORITY_CHOICES=[
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
]
STATUS_CHOICES=[
('PENDING', 'Pending'),
('SCHEDULED', 'Scheduled'),
('CONFIRMED', 'Confirmed'),
('CHECKED_IN', 'Checked In'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('NO_SHOW', 'No Show'),
('RESCHEDULED', 'Rescheduled'),
]
TELEMEDICINE_PLATFORM_CHOICES=[
('ZOOM', 'Zoom'),
('TEAMS', 'Microsoft Teams'),
('WEBEX', 'Cisco Webex'),
('DOXY', 'Doxy.me'),
('CUSTOM', 'Custom Platform'),
('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=APPOINTMENT_TYPE_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='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=STATUS_CHOICES,
default='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=TELEMEDICINE_PLATFORM_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
class SlotAvailability(models.Model):
"""
Provider availability slots for appointment scheduling.
"""
AVAILABILITY_TYPE_CHOICES=[
('REGULAR', 'Regular Hours'),
('EXTENDED', 'Extended Hours'),
('EMERGENCY', 'Emergency'),
('ON_CALL', 'On Call'),
('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=AVAILABILITY_TYPE_CHOICES,
default='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.
"""
QUEUE_TYPE_CHOICES=[
('PROVIDER', 'Provider Queue'),
('SPECIALTY', 'Specialty Queue'),
('LOCATION', 'Location Queue'),
('PROCEDURE', 'Procedure Queue'),
('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=QUEUE_TYPE_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
class QueueEntry(models.Model):
"""
Individual entry in a waiting queue.
"""
STATUS_CHOICES=[
('WAITING', 'Waiting'),
('CALLED', 'Called'),
('IN_SERVICE', 'In Service'),
('COMPLETED', 'Completed'),
('LEFT', 'Left Queue'),
('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=STATUS_CHOICES,
default='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
class TelemedicineSession(models.Model):
"""
Telemedicine session management.
"""
PLATFORM_CHOICES=[
('ZOOM', 'Zoom'),
('TEAMS', 'Microsoft Teams'),
('WEBEX', 'Cisco Webex'),
('DOXY', 'Doxy.me'),
('CUSTOM', 'Custom Platform'),
('OTHER', 'Other'),
]
STATUS_CHOICES=[
('SCHEDULED', 'Scheduled'),
('READY', 'Ready to Start'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('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=STATUS_CHOICES,
default='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
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.
"""
APPOINTMENT_TYPE_CHOICES = [
('CONSULTATION', 'Consultation'),
('FOLLOW_UP', 'Follow-up'),
('PROCEDURE', 'Procedure'),
('SURGERY', 'Surgery'),
('DIAGNOSTIC', 'Diagnostic'),
('THERAPY', 'Therapy'),
('VACCINATION', 'Vaccination'),
('SCREENING', 'Screening'),
('EMERGENCY', 'Emergency'),
('TELEMEDICINE', 'Telemedicine'),
('OTHER', 'Other'),
]
SPECIALTY_CHOICES = [
('FAMILY_MEDICINE', 'Family Medicine'),
('INTERNAL_MEDICINE', 'Internal Medicine'),
('PEDIATRICS', 'Pediatrics'),
('CARDIOLOGY', 'Cardiology'),
('DERMATOLOGY', 'Dermatology'),
('ENDOCRINOLOGY', 'Endocrinology'),
('GASTROENTEROLOGY', 'Gastroenterology'),
('NEUROLOGY', 'Neurology'),
('ONCOLOGY', 'Oncology'),
('ORTHOPEDICS', 'Orthopedics'),
('PSYCHIATRY', 'Psychiatry'),
('RADIOLOGY', 'Radiology'),
('SURGERY', 'Surgery'),
('UROLOGY', 'Urology'),
('GYNECOLOGY', 'Gynecology'),
('OPHTHALMOLOGY', 'Ophthalmology'),
('ENT', 'Ear, Nose & Throat'),
('EMERGENCY', 'Emergency Medicine'),
('OTHER', 'Other'),
]
PRIORITY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
]
CONTACT_METHOD_CHOICES = [
('PHONE', 'Phone'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PORTAL', 'Patient Portal'),
('MAIL', 'Mail'),
]
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('CONTACTED', 'Contacted'),
('OFFERED', 'Appointment Offered'),
('SCHEDULED', 'Scheduled'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
('TRANSFERRED', 'Transferred'),
]
AUTHORIZATION_STATUS_CHOICES = [
('NOT_REQUIRED', 'Not Required'),
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('DENIED', 'Denied'),
('EXPIRED', 'Expired'),
]
REFERRAL_URGENCY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
]
REMOVAL_REASON_CHOICES = [
('SCHEDULED', 'Appointment Scheduled'),
('PATIENT_CANCELLED', 'Patient Cancelled'),
('PROVIDER_CANCELLED', 'Provider Cancelled'),
('NO_RESPONSE', 'No Response to Contact'),
('INSURANCE_ISSUE', 'Insurance Issue'),
('TRANSFERRED', 'Transferred to Another Provider'),
('EXPIRED', 'Entry Expired'),
('DUPLICATE', 'Duplicate Entry'),
('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=APPOINTMENT_TYPE_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='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=CONTACT_METHOD_CHOICES,
default='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=STATUS_CHOICES,
default='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=AUTHORIZATION_STATUS_CHOICES,
default='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=REFERRAL_URGENCY_CHOICES,
default='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=REMOVAL_REASON_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.
"""
CONTACT_METHOD_CHOICES = [
('PHONE', 'Phone Call'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PORTAL', 'Patient Portal Message'),
('MAIL', 'Mail'),
('IN_PERSON', 'In Person'),
]
CONTACT_OUTCOME_CHOICES = [
('SUCCESSFUL', 'Successful Contact'),
('NO_ANSWER', 'No Answer'),
('BUSY', 'Line Busy'),
('VOICEMAIL', 'Left Voicemail'),
('EMAIL_SENT', 'Email Sent'),
('EMAIL_BOUNCED', 'Email Bounced'),
('SMS_SENT', 'SMS Sent'),
('SMS_FAILED', 'SMS Failed'),
('WRONG_NUMBER', 'Wrong Number'),
('DECLINED', 'Patient Declined'),
]
PATIENT_RESPONSE_CHOICES = [
('ACCEPTED', 'Accepted Appointment'),
('DECLINED', 'Declined Appointment'),
('REQUESTED_DIFFERENT', 'Requested Different Time'),
('WILL_CALL_BACK', 'Will Call Back'),
('NO_LONGER_NEEDED', 'No Longer Needed'),
('INSURANCE_ISSUE', 'Insurance Issue'),
('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=CONTACT_METHOD_CHOICES,
help_text='Method of contact used'
)
contact_outcome = models.CharField(
max_length=20,
choices=CONTACT_OUTCOME_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=PATIENT_RESPONSE_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})"