1179 lines
32 KiB
Python
1179 lines
32 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})"
|
|
|