diff --git a/.DS_Store b/.DS_Store
index a9edf1ea..ff1afbfd 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/appointments/__pycache__/admin.cpython-312.pyc b/appointments/__pycache__/admin.cpython-312.pyc
index 647d5ed5..e062354c 100644
Binary files a/appointments/__pycache__/admin.cpython-312.pyc and b/appointments/__pycache__/admin.cpython-312.pyc differ
diff --git a/appointments/__pycache__/forms.cpython-312.pyc b/appointments/__pycache__/forms.cpython-312.pyc
index f955dbd7..0b8962c8 100644
Binary files a/appointments/__pycache__/forms.cpython-312.pyc and b/appointments/__pycache__/forms.cpython-312.pyc differ
diff --git a/appointments/__pycache__/models.cpython-312.pyc b/appointments/__pycache__/models.cpython-312.pyc
index 2870cc34..d253311d 100644
Binary files a/appointments/__pycache__/models.cpython-312.pyc and b/appointments/__pycache__/models.cpython-312.pyc differ
diff --git a/appointments/__pycache__/urls.cpython-312.pyc b/appointments/__pycache__/urls.cpython-312.pyc
index 8f5fee78..a08b1834 100644
Binary files a/appointments/__pycache__/urls.cpython-312.pyc and b/appointments/__pycache__/urls.cpython-312.pyc differ
diff --git a/appointments/__pycache__/views.cpython-312.pyc b/appointments/__pycache__/views.cpython-312.pyc
index d66e29ff..0c64ffa1 100644
Binary files a/appointments/__pycache__/views.cpython-312.pyc and b/appointments/__pycache__/views.cpython-312.pyc differ
diff --git a/appointments/admin.py b/appointments/admin.py
index 386132b7..332aa571 100644
--- a/appointments/admin.py
+++ b/appointments/admin.py
@@ -5,10 +5,7 @@ Admin configuration for appointments app.
from django.contrib import admin
from django.utils.html import format_html
from django.utils import timezone
-from .models import (
- AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
- TelemedicineSession, AppointmentTemplate
-)
+from .models import *
class QueueEntryInline(admin.TabularInline):
@@ -459,3 +456,151 @@ class AppointmentTemplateAdmin(admin.ModelAdmin):
'tenant', 'created_by'
)
+
+@admin.register(WaitingList)
+class WaitingListAdmin(admin.ModelAdmin):
+ """
+ Admin configuration for WaitingList model.
+ """
+ list_display = [
+ 'patient', 'specialty', 'priority', 'urgency_score',
+ 'status', 'position', 'days_waiting', 'last_contacted'
+ ]
+ list_filter = [
+ 'tenant', 'department', 'specialty', 'priority', 'status',
+ 'appointment_type', 'authorization_required', 'requires_interpreter'
+ ]
+ search_fields = [
+ 'patient__first_name', 'patient__last_name', 'patient__mrn',
+ 'clinical_indication', 'referring_provider'
+ ]
+ ordering = ['priority', 'urgency_score', 'created_at']
+
+ fieldsets = (
+ ('Patient Information', {
+ 'fields': ('tenant', 'patient', 'department', 'provider')
+ }),
+ ('Service Request', {
+ 'fields': ('appointment_type', 'specialty', 'clinical_indication')
+ }),
+ ('Priority and Urgency', {
+ 'fields': ('priority', 'urgency_score', 'diagnosis_codes')
+ }),
+ ('Patient Preferences', {
+ 'fields': (
+ 'preferred_date', 'preferred_time', 'flexible_scheduling',
+ 'earliest_acceptable_date', 'latest_acceptable_date'
+ )
+ }),
+ ('Contact Information', {
+ 'fields': ('contact_method', 'contact_phone', 'contact_email')
+ }),
+ ('Status and Position', {
+ 'fields': ('status', 'position', 'estimated_wait_time')
+ }),
+ ('Contact History', {
+ 'fields': (
+ 'last_contacted', 'contact_attempts', 'max_contact_attempts',
+ 'appointments_offered', 'appointments_declined'
+ )
+ }),
+ ('Requirements', {
+ 'fields': (
+ 'requires_interpreter', 'interpreter_language',
+ 'accessibility_requirements', 'transportation_needed'
+ ),
+ 'classes': ('collapse',)
+ }),
+ ('Insurance and Authorization', {
+ 'fields': (
+ 'insurance_verified', 'authorization_required',
+ 'authorization_status', 'authorization_number'
+ ),
+ 'classes': ('collapse',)
+ }),
+ ('Referral Information', {
+ 'fields': (
+ 'referring_provider', 'referral_date', 'referral_urgency'
+ ),
+ 'classes': ('collapse',)
+ }),
+ ('Outcome', {
+ 'fields': (
+ 'scheduled_appointment', 'removal_reason', 'removal_notes',
+ 'removed_at', 'removed_by'
+ ),
+ 'classes': ('collapse',)
+ }),
+ ('Notes', {
+ 'fields': ('notes',)
+ }),
+ ('Metadata', {
+ 'fields': ('waiting_list_id', 'created_at', 'updated_at', 'created_by'),
+ 'classes': ('collapse',)
+ }),
+ )
+
+ readonly_fields = ['waiting_list_id', 'created_at', 'updated_at', 'days_waiting']
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related(
+ 'tenant', 'patient', 'department', 'provider', 'created_by',
+ 'removed_by', 'scheduled_appointment'
+ )
+
+ def days_waiting(self, obj):
+ return obj.days_waiting
+
+ days_waiting.short_description = 'Days Waiting'
+
+
+@admin.register(WaitingListContactLog)
+class WaitingListContactLogAdmin(admin.ModelAdmin):
+ """
+ Admin configuration for WaitingListContactLog model.
+ """
+ list_display = [
+ 'waiting_list_entry', 'contact_date', 'contact_method',
+ 'contact_outcome', 'appointment_offered', 'patient_response'
+ ]
+ list_filter = [
+ 'contact_method', 'contact_outcome', 'appointment_offered',
+ 'patient_response', 'contact_date'
+ ]
+ search_fields = [
+ 'waiting_list_entry__patient__first_name',
+ 'waiting_list_entry__patient__last_name',
+ 'notes'
+ ]
+ ordering = ['-contact_date']
+
+ fieldsets = (
+ ('Contact Information', {
+ 'fields': ('waiting_list_entry', 'contact_method', 'contact_outcome')
+ }),
+ ('Appointment Offer', {
+ 'fields': (
+ 'appointment_offered', 'offered_date', 'offered_time',
+ 'patient_response'
+ )
+ }),
+ ('Follow-up', {
+ 'fields': ('next_contact_date',)
+ }),
+ ('Notes', {
+ 'fields': ('notes',)
+ }),
+ ('Metadata', {
+ 'fields': ('contact_date', 'contacted_by'),
+ 'classes': ('collapse',)
+ }),
+ )
+
+ readonly_fields = ['contact_date']
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related(
+ 'waiting_list_entry__patient', 'contacted_by'
+ )
+
+
diff --git a/appointments/forms.py b/appointments/forms.py
index d6172ba9..dc0514ee 100644
--- a/appointments/forms.py
+++ b/appointments/forms.py
@@ -5,15 +5,11 @@ Forms for Appointments app CRUD operations.
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
-from datetime import datetime, time, timedelta
-from .models import (
- AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
- TelemedicineSession, AppointmentTemplate
-)
+from datetime import datetime, time, timedelta, date
+from .models import *
from patients.models import PatientProfile
from accounts.models import User
-from hr.models import Employee
-
+from hr.models import Employee, Department
class AppointmentRequestForm(forms.ModelForm):
"""
@@ -361,8 +357,6 @@ class AppointmentSearchForm(forms.Form):
).order_by('last_name', 'first_name')
-
-
class QueueSearchForm(forms.Form):
"""
Form for searching queues.
@@ -441,6 +435,437 @@ class SlotSearchForm(forms.Form):
).order_by('last_name', 'first_name')
+class WaitingListForm(forms.ModelForm):
+ """
+ Form for creating and updating waiting list entries.
+ """
+
+ class Meta:
+ model = WaitingList
+ fields = [
+ 'patient', 'department', 'provider', 'appointment_type', 'specialty',
+ 'priority', 'urgency_score', 'clinical_indication', 'diagnosis_codes',
+ 'preferred_date', 'preferred_time', 'flexible_scheduling',
+ 'earliest_acceptable_date', 'latest_acceptable_date',
+ 'acceptable_days', 'acceptable_times',
+ 'contact_method', 'contact_phone', 'contact_email',
+ 'requires_interpreter', 'interpreter_language',
+ 'accessibility_requirements', 'transportation_needed',
+ 'insurance_verified', 'authorization_required',
+ 'referring_provider', 'referral_date', 'referral_urgency',
+ 'notes'
+ ]
+
+ widgets = {
+ 'patient': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'department': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'provider': forms.Select(attrs={
+ 'class': 'form-select'
+ }),
+ 'appointment_type': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'specialty': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'priority': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'urgency_score': forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'min': 1,
+ 'max': 10,
+ 'required': True
+ }),
+ 'clinical_indication': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 4,
+ 'required': True,
+ 'placeholder': 'Describe the clinical reason for this appointment request...'
+ }),
+ 'diagnosis_codes': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 2,
+ 'placeholder': 'Enter ICD-10 codes separated by commas'
+ }),
+ 'preferred_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date',
+ 'min': date.today().isoformat()
+ }),
+ 'preferred_time': forms.TimeInput(attrs={
+ 'class': 'form-control',
+ 'type': 'time'
+ }),
+ 'flexible_scheduling': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'earliest_acceptable_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date',
+ 'min': date.today().isoformat()
+ }),
+ 'latest_acceptable_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ }),
+ 'acceptable_days': forms.CheckboxSelectMultiple(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'contact_method': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'contact_phone': forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': '(555) 123-4567'
+ }),
+ 'contact_email': forms.EmailInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'patient@example.com'
+ }),
+ 'requires_interpreter': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'interpreter_language': forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'e.g., Spanish, Mandarin, ASL'
+ }),
+ 'accessibility_requirements': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 3,
+ 'placeholder': 'Describe any accessibility needs...'
+ }),
+ 'transportation_needed': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'insurance_verified': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'authorization_required': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'referring_provider': forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'Dr. Smith, Internal Medicine'
+ }),
+ 'referral_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ }),
+ 'referral_urgency': forms.Select(attrs={
+ 'class': 'form-select'
+ }),
+ 'notes': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 4,
+ 'placeholder': 'Additional notes and comments...'
+ }),
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.tenant = kwargs.pop('tenant', None)
+ super().__init__(*args, **kwargs)
+
+ # Filter choices based on tenant
+ if self.tenant:
+ self.fields['patient'].queryset = self.fields['patient'].queryset.filter(
+ tenant=self.tenant
+ )
+ self.fields['department'].queryset = self.fields['department'].queryset.filter(
+ tenant=self.tenant
+ )
+ if 'provider' in self.fields:
+ self.fields['provider'].queryset = self.fields['provider'].queryset.filter(
+ tenant=self.tenant
+ )
+
+ def clean(self):
+ cleaned_data = super().clean()
+
+ # Validate date ranges
+ preferred_date = cleaned_data.get('preferred_date')
+ earliest_date = cleaned_data.get('earliest_acceptable_date')
+ latest_date = cleaned_data.get('latest_acceptable_date')
+
+ if preferred_date and preferred_date < date.today():
+ raise ValidationError("Preferred date cannot be in the past.")
+
+ if earliest_date and latest_date and earliest_date > latest_date:
+ raise ValidationError("Earliest acceptable date must be before latest acceptable date.")
+
+ if preferred_date and earliest_date and preferred_date < earliest_date:
+ raise ValidationError("Preferred date must be within acceptable date range.")
+
+ if preferred_date and latest_date and preferred_date > latest_date:
+ raise ValidationError("Preferred date must be within acceptable date range.")
+
+ # Validate contact information
+ contact_method = cleaned_data.get('contact_method')
+ contact_phone = cleaned_data.get('contact_phone')
+ contact_email = cleaned_data.get('contact_email')
+
+ if contact_method == 'PHONE' and not contact_phone:
+ raise ValidationError("Phone number is required when phone is selected as contact method.")
+
+ if contact_method == 'EMAIL' and not contact_email:
+ raise ValidationError("Email address is required when email is selected as contact method.")
+
+ # Validate interpreter requirements
+ requires_interpreter = cleaned_data.get('requires_interpreter')
+ interpreter_language = cleaned_data.get('interpreter_language')
+
+ if requires_interpreter and not interpreter_language:
+ raise ValidationError("Interpreter language is required when interpreter services are needed.")
+
+ return cleaned_data
+
+
+class WaitingListContactLogForm(forms.ModelForm):
+ """
+ Form for logging contact attempts with waiting list patients.
+ """
+
+ class Meta:
+ model = WaitingListContactLog
+ fields = [
+ 'contact_method', 'contact_outcome', 'appointment_offered',
+ 'offered_date', 'offered_time', 'patient_response',
+ 'notes', 'next_contact_date'
+ ]
+
+ widgets = {
+ 'contact_method': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'contact_outcome': forms.Select(attrs={
+ 'class': 'form-select',
+ 'required': True
+ }),
+ 'appointment_offered': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ 'offered_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ }),
+ 'offered_time': forms.TimeInput(attrs={
+ 'class': 'form-control',
+ 'type': 'time'
+ }),
+ 'patient_response': forms.Select(attrs={
+ 'class': 'form-select'
+ }),
+ 'notes': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 4,
+ 'placeholder': 'Notes from contact attempt...'
+ }),
+ 'next_contact_date': forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date',
+ 'min': date.today().isoformat()
+ }),
+ }
+
+ def clean(self):
+ cleaned_data = super().clean()
+
+ appointment_offered = cleaned_data.get('appointment_offered')
+ offered_date = cleaned_data.get('offered_date')
+ offered_time = cleaned_data.get('offered_time')
+ patient_response = cleaned_data.get('patient_response')
+
+ if appointment_offered:
+ if not offered_date:
+ raise ValidationError("Offered date is required when appointment is offered.")
+ if not offered_time:
+ raise ValidationError("Offered time is required when appointment is offered.")
+ if not patient_response:
+ raise ValidationError("Patient response is required when appointment is offered.")
+
+ next_contact_date = cleaned_data.get('next_contact_date')
+ if next_contact_date and next_contact_date < date.today():
+ raise ValidationError("Next contact date cannot be in the past.")
+
+ return cleaned_data
+
+
+class WaitingListFilterForm(forms.Form):
+ """
+ Form for filtering waiting list entries.
+ """
+
+ PRIORITY_CHOICES = [
+ ('', 'All Priorities'),
+ ('EMERGENCY', 'Emergency'),
+ ('STAT', 'STAT'),
+ ('URGENT', 'Urgent'),
+ ('ROUTINE', 'Routine'),
+ ]
+
+ STATUS_CHOICES = [
+ ('', 'All Status'),
+ ('ACTIVE', 'Active'),
+ ('CONTACTED', 'Contacted'),
+ ('OFFERED', 'Appointment Offered'),
+ ('SCHEDULED', 'Scheduled'),
+ ('CANCELLED', 'Cancelled'),
+ ('EXPIRED', 'Expired'),
+ ]
+
+ department = forms.ModelChoiceField(
+ queryset=None,
+ required=False,
+ empty_label="All Departments",
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ specialty = forms.ChoiceField(
+ choices=[('', 'All Specialties')] + WaitingList._meta.get_field('specialty').choices,
+ required=False,
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ priority = forms.ChoiceField(
+ choices=PRIORITY_CHOICES,
+ required=False,
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ status = forms.ChoiceField(
+ choices=STATUS_CHOICES,
+ required=False,
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ provider = forms.ModelChoiceField(
+ queryset=None,
+ required=False,
+ empty_label="All Providers",
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ date_from = forms.DateField(
+ required=False,
+ widget=forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ })
+ )
+
+ date_to = forms.DateField(
+ required=False,
+ widget=forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ })
+ )
+
+ urgency_min = forms.IntegerField(
+ required=False,
+ min_value=1,
+ max_value=10,
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'Min urgency (1-10)'
+ })
+ )
+
+ urgency_max = forms.IntegerField(
+ required=False,
+ min_value=1,
+ max_value=10,
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'Max urgency (1-10)'
+ })
+ )
+
+ def __init__(self, *args, **kwargs):
+ tenant = kwargs.pop('tenant', None)
+ super().__init__(*args, **kwargs)
+
+ if tenant:
+ self.fields['department'].queryset = Department.objects.filter(tenant=tenant)
+ self.fields['provider'].queryset = User.objects.filter(
+ tenant=tenant
+ )
+
+
+class WaitingListBulkActionForm(forms.Form):
+ """
+ Form for bulk actions on waiting list entries.
+ """
+
+ ACTION_CHOICES = [
+ ('', 'Select Action'),
+ ('contact', 'Mark as Contacted'),
+ ('cancel', 'Cancel Entries'),
+ ('update_priority', 'Update Priority'),
+ ('transfer_provider', 'Transfer to Provider'),
+ ('export', 'Export Selected'),
+ ]
+
+ action = forms.ChoiceField(
+ choices=ACTION_CHOICES,
+ required=True,
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ # Fields for specific actions
+ new_priority = forms.ChoiceField(
+ choices=WaitingList._meta.get_field('priority').choices,
+ required=False,
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ transfer_provider = forms.ModelChoiceField(
+ queryset=None,
+ required=False,
+ widget=forms.Select(attrs={'class': 'form-select'})
+ )
+
+ contact_notes = forms.CharField(
+ required=False,
+ widget=forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 3,
+ 'placeholder': 'Notes for contact action...'
+ })
+ )
+
+ cancellation_reason = forms.CharField(
+ required=False,
+ widget=forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 3,
+ 'placeholder': 'Reason for cancellation...'
+ })
+ )
+
+ def __init__(self, *args, **kwargs):
+ tenant = kwargs.pop('tenant', None)
+ super().__init__(*args, **kwargs)
+
+ if tenant:
+ from django.contrib.auth import get_user_model
+ User = get_user_model()
+
+ self.fields['transfer_provider'].queryset = User.objects.filter(
+ tenant=tenant
+ )
+
# from django import forms
# from django.core.exceptions import ValidationError
# from django.utils import timezone
diff --git a/appointments/migrations/0003_waitinglist_waitinglistcontactlog_and_more.py b/appointments/migrations/0003_waitinglist_waitinglistcontactlog_and_more.py
new file mode 100644
index 00000000..7b3fa2ed
--- /dev/null
+++ b/appointments/migrations/0003_waitinglist_waitinglistcontactlog_and_more.py
@@ -0,0 +1,688 @@
+# Generated by Django 5.2.6 on 2025-09-11 17:03
+
+import django.core.validators
+import django.db.models.deletion
+import uuid
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("appointments", "0002_initial"),
+ ("core", "0001_initial"),
+ ("hr", "0001_initial"),
+ ("patients", "0003_remove_insuranceinfo_subscriber_ssn_and_more"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="WaitingList",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "waiting_list_id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ help_text="Unique waiting list entry identifier",
+ unique=True,
+ ),
+ ),
+ (
+ "appointment_type",
+ models.CharField(
+ 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"),
+ ],
+ help_text="Type of appointment requested",
+ max_length=50,
+ ),
+ ),
+ (
+ "specialty",
+ models.CharField(
+ 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"),
+ ],
+ help_text="Medical specialty required",
+ max_length=100,
+ ),
+ ),
+ (
+ "priority",
+ models.CharField(
+ choices=[
+ ("ROUTINE", "Routine"),
+ ("URGENT", "Urgent"),
+ ("STAT", "STAT"),
+ ("EMERGENCY", "Emergency"),
+ ],
+ default="ROUTINE",
+ help_text="Clinical priority level",
+ max_length=20,
+ ),
+ ),
+ (
+ "urgency_score",
+ models.PositiveIntegerField(
+ default=1,
+ help_text="Clinical urgency score (1-10, 10 being most urgent)",
+ validators=[
+ django.core.validators.MinValueValidator(1),
+ django.core.validators.MaxValueValidator(10),
+ ],
+ ),
+ ),
+ (
+ "clinical_indication",
+ models.TextField(
+ help_text="Clinical reason for appointment request"
+ ),
+ ),
+ (
+ "diagnosis_codes",
+ models.JSONField(
+ blank=True, default=list, help_text="ICD-10 diagnosis codes"
+ ),
+ ),
+ (
+ "preferred_date",
+ models.DateField(
+ blank=True,
+ help_text="Patient preferred appointment date",
+ null=True,
+ ),
+ ),
+ (
+ "preferred_time",
+ models.TimeField(
+ blank=True,
+ help_text="Patient preferred appointment time",
+ null=True,
+ ),
+ ),
+ (
+ "flexible_scheduling",
+ models.BooleanField(
+ default=True,
+ help_text="Patient accepts alternative dates/times",
+ ),
+ ),
+ (
+ "earliest_acceptable_date",
+ models.DateField(
+ blank=True,
+ help_text="Earliest acceptable appointment date",
+ null=True,
+ ),
+ ),
+ (
+ "latest_acceptable_date",
+ models.DateField(
+ blank=True,
+ help_text="Latest acceptable appointment date",
+ null=True,
+ ),
+ ),
+ (
+ "acceptable_days",
+ models.JSONField(
+ blank=True,
+ default=list,
+ help_text="Acceptable days of week (0=Monday, 6=Sunday)",
+ ),
+ ),
+ (
+ "acceptable_times",
+ models.JSONField(
+ blank=True, default=list, help_text="Acceptable time ranges"
+ ),
+ ),
+ (
+ "contact_method",
+ models.CharField(
+ choices=[
+ ("PHONE", "Phone"),
+ ("EMAIL", "Email"),
+ ("SMS", "SMS"),
+ ("PORTAL", "Patient Portal"),
+ ("MAIL", "Mail"),
+ ],
+ default="PHONE",
+ help_text="Preferred contact method",
+ max_length=20,
+ ),
+ ),
+ (
+ "contact_phone",
+ models.CharField(
+ blank=True,
+ help_text="Contact phone number",
+ max_length=20,
+ null=True,
+ ),
+ ),
+ (
+ "contact_email",
+ models.EmailField(
+ blank=True,
+ help_text="Contact email address",
+ max_length=254,
+ null=True,
+ ),
+ ),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("ACTIVE", "Active"),
+ ("CONTACTED", "Contacted"),
+ ("OFFERED", "Appointment Offered"),
+ ("SCHEDULED", "Scheduled"),
+ ("CANCELLED", "Cancelled"),
+ ("EXPIRED", "Expired"),
+ ("TRANSFERRED", "Transferred"),
+ ],
+ default="ACTIVE",
+ help_text="Waiting list status",
+ max_length=20,
+ ),
+ ),
+ (
+ "position",
+ models.PositiveIntegerField(
+ blank=True,
+ help_text="Position in waiting list queue",
+ null=True,
+ ),
+ ),
+ (
+ "estimated_wait_time",
+ models.PositiveIntegerField(
+ blank=True, help_text="Estimated wait time in days", null=True
+ ),
+ ),
+ (
+ "last_contacted",
+ models.DateTimeField(
+ blank=True,
+ help_text="Last contact attempt date/time",
+ null=True,
+ ),
+ ),
+ (
+ "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"
+ ),
+ ),
+ (
+ "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,
+ help_text="Date of last appointment offer",
+ null=True,
+ ),
+ ),
+ (
+ "requires_interpreter",
+ models.BooleanField(
+ default=False, help_text="Patient requires interpreter services"
+ ),
+ ),
+ (
+ "interpreter_language",
+ models.CharField(
+ blank=True,
+ help_text="Required interpreter language",
+ max_length=50,
+ null=True,
+ ),
+ ),
+ (
+ "accessibility_requirements",
+ models.TextField(
+ blank=True,
+ help_text="Special accessibility requirements",
+ null=True,
+ ),
+ ),
+ (
+ "transportation_needed",
+ models.BooleanField(
+ default=False,
+ help_text="Patient needs transportation assistance",
+ ),
+ ),
+ (
+ "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(
+ choices=[
+ ("NOT_REQUIRED", "Not Required"),
+ ("PENDING", "Pending"),
+ ("APPROVED", "Approved"),
+ ("DENIED", "Denied"),
+ ("EXPIRED", "Expired"),
+ ],
+ default="NOT_REQUIRED",
+ help_text="Authorization status",
+ max_length=20,
+ ),
+ ),
+ (
+ "authorization_number",
+ models.CharField(
+ blank=True,
+ help_text="Authorization number",
+ max_length=100,
+ null=True,
+ ),
+ ),
+ (
+ "referring_provider",
+ models.CharField(
+ blank=True,
+ help_text="Referring provider name",
+ max_length=200,
+ null=True,
+ ),
+ ),
+ (
+ "referral_date",
+ models.DateField(
+ blank=True, help_text="Date of referral", null=True
+ ),
+ ),
+ (
+ "referral_urgency",
+ models.CharField(
+ choices=[
+ ("ROUTINE", "Routine"),
+ ("URGENT", "Urgent"),
+ ("STAT", "STAT"),
+ ],
+ default="ROUTINE",
+ help_text="Referral urgency level",
+ max_length=20,
+ ),
+ ),
+ (
+ "removal_reason",
+ models.CharField(
+ blank=True,
+ 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"),
+ ],
+ help_text="Reason for removal from waiting list",
+ max_length=50,
+ null=True,
+ ),
+ ),
+ (
+ "removal_notes",
+ models.TextField(
+ blank=True,
+ help_text="Additional notes about removal",
+ null=True,
+ ),
+ ),
+ (
+ "removed_at",
+ models.DateTimeField(
+ blank=True,
+ help_text="Date/time removed from waiting list",
+ null=True,
+ ),
+ ),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ (
+ "notes",
+ models.TextField(
+ blank=True, help_text="Additional notes and comments", null=True
+ ),
+ ),
+ (
+ "created_by",
+ models.ForeignKey(
+ blank=True,
+ help_text="User who created the waiting list entry",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="created_waiting_list_entries",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "department",
+ models.ForeignKey(
+ help_text="Department for appointment",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="waiting_list_entries",
+ to="hr.department",
+ ),
+ ),
+ (
+ "patient",
+ models.ForeignKey(
+ help_text="Patient on waiting list",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="waiting_list_entries",
+ to="patients.patientprofile",
+ ),
+ ),
+ (
+ "provider",
+ models.ForeignKey(
+ blank=True,
+ help_text="Preferred healthcare provider",
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="provider_waiting_list",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "removed_by",
+ models.ForeignKey(
+ blank=True,
+ help_text="User who removed entry from waiting list",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="removed_waiting_list_entries",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "scheduled_appointment",
+ models.ForeignKey(
+ blank=True,
+ help_text="Scheduled appointment from waiting list",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="waiting_list_entry",
+ to="appointments.appointmentrequest",
+ ),
+ ),
+ (
+ "tenant",
+ models.ForeignKey(
+ help_text="Organization tenant",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="waiting_list_entries",
+ to="core.tenant",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Waiting List Entry",
+ "verbose_name_plural": "Waiting List Entries",
+ "db_table": "appointments_waiting_list",
+ "ordering": ["priority", "urgency_score", "created_at"],
+ },
+ ),
+ migrations.CreateModel(
+ name="WaitingListContactLog",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "contact_date",
+ models.DateTimeField(
+ auto_now_add=True, help_text="Date and time of contact attempt"
+ ),
+ ),
+ (
+ "contact_method",
+ models.CharField(
+ choices=[
+ ("PHONE", "Phone Call"),
+ ("EMAIL", "Email"),
+ ("SMS", "SMS"),
+ ("PORTAL", "Patient Portal Message"),
+ ("MAIL", "Mail"),
+ ("IN_PERSON", "In Person"),
+ ],
+ help_text="Method of contact used",
+ max_length=20,
+ ),
+ ),
+ (
+ "contact_outcome",
+ models.CharField(
+ 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"),
+ ],
+ help_text="Outcome of contact attempt",
+ max_length=20,
+ ),
+ ),
+ (
+ "appointment_offered",
+ models.BooleanField(
+ default=False,
+ help_text="Appointment was offered during contact",
+ ),
+ ),
+ (
+ "offered_date",
+ models.DateField(
+ blank=True, help_text="Date of offered appointment", null=True
+ ),
+ ),
+ (
+ "offered_time",
+ models.TimeField(
+ blank=True, help_text="Time of offered appointment", null=True
+ ),
+ ),
+ (
+ "patient_response",
+ models.CharField(
+ blank=True,
+ 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"),
+ ],
+ help_text="Patient response to contact",
+ max_length=20,
+ null=True,
+ ),
+ ),
+ (
+ "notes",
+ models.TextField(
+ blank=True, help_text="Notes from contact attempt", null=True
+ ),
+ ),
+ (
+ "next_contact_date",
+ models.DateField(
+ blank=True,
+ help_text="Scheduled date for next contact attempt",
+ null=True,
+ ),
+ ),
+ (
+ "contacted_by",
+ models.ForeignKey(
+ blank=True,
+ help_text="Staff member who made contact",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "waiting_list_entry",
+ models.ForeignKey(
+ help_text="Associated waiting list entry",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="contact_logs",
+ to="appointments.waitinglist",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Waiting List Contact Log",
+ "verbose_name_plural": "Waiting List Contact Logs",
+ "db_table": "appointments_waiting_list_contact_log",
+ "ordering": ["-contact_date"],
+ },
+ ),
+ migrations.AddIndex(
+ model_name="waitinglist",
+ index=models.Index(
+ fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglist",
+ index=models.Index(
+ fields=["patient", "status"], name="appointment_patient_73f03d_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglist",
+ index=models.Index(
+ fields=["department", "specialty", "status"],
+ name="appointment_departm_78fd70_idx",
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglist",
+ index=models.Index(
+ fields=["priority", "urgency_score"],
+ name="appointment_priorit_30fb90_idx",
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglist",
+ index=models.Index(
+ fields=["status", "created_at"], name="appointment_status_cfe551_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglist",
+ index=models.Index(
+ fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglistcontactlog",
+ index=models.Index(
+ fields=["waiting_list_entry", "contact_date"],
+ name="appointment_waiting_50d8ac_idx",
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglistcontactlog",
+ index=models.Index(
+ fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="waitinglistcontactlog",
+ index=models.Index(
+ fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
+ ),
+ ),
+ ]
diff --git a/appointments/migrations/__pycache__/0003_waitinglist_waitinglistcontactlog_and_more.cpython-312.pyc b/appointments/migrations/__pycache__/0003_waitinglist_waitinglistcontactlog_and_more.cpython-312.pyc
new file mode 100644
index 00000000..12a8e50c
Binary files /dev/null and b/appointments/migrations/__pycache__/0003_waitinglist_waitinglistcontactlog_and_more.cpython-312.pyc differ
diff --git a/appointments/models.py b/appointments/models.py
index 6b1c8f5d..edb37303 100644
--- a/appointments/models.py
+++ b/appointments/models.py
@@ -1176,3 +1176,640 @@ class AppointmentTemplate(models.Model):
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,
+ 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})"
+
diff --git a/appointments/templates/.DS_Store b/appointments/templates/.DS_Store
index 92b5a68f..6f9a9259 100644
Binary files a/appointments/templates/.DS_Store and b/appointments/templates/.DS_Store differ
diff --git a/appointments/templates/appointments/.DS_Store b/appointments/templates/appointments/.DS_Store
index 74dbb686..4b8879d8 100644
Binary files a/appointments/templates/appointments/.DS_Store and b/appointments/templates/appointments/.DS_Store differ
diff --git a/appointments/templates/appointments/partials/contact_log_list.html b/appointments/templates/appointments/partials/contact_log_list.html
new file mode 100644
index 00000000..9d8a5588
--- /dev/null
+++ b/appointments/templates/appointments/partials/contact_log_list.html
@@ -0,0 +1,39 @@
+{% for log in contact_logs %}
+
+{% empty %}
+
+
+
No contact logs available for this entry.
+
+{% endfor %}
+
diff --git a/appointments/templates/appointments/queue/waiting_queue_form.html b/appointments/templates/appointments/queue/waiting_queue_form.html
index ff2b5d6f..0d1b68ed 100644
--- a/appointments/templates/appointments/queue/waiting_queue_form.html
+++ b/appointments/templates/appointments/queue/waiting_queue_form.html
@@ -151,22 +151,15 @@
{% csrf_token %}
-