1817 lines
50 KiB
Python
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})"
|
|
|