1076 lines
40 KiB
Python
1076 lines
40 KiB
Python
"""
|
|
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, date
|
|
from .models import *
|
|
from patients.models import PatientProfile
|
|
from accounts.models import User
|
|
from hr.models import Employee, Department
|
|
|
|
class AppointmentRequestForm(forms.ModelForm):
|
|
"""
|
|
Form for appointment request management.
|
|
Patient is set automatically in the view; it is NOT exposed here.
|
|
"""
|
|
class Meta:
|
|
model = AppointmentRequest
|
|
# do not include 'patient' so it cannot be tampered with
|
|
exclude = ['patient','status']
|
|
fields = [
|
|
'provider', 'appointment_type', 'specialty', 'preferred_date',
|
|
'preferred_time', 'duration_minutes', 'priority', 'chief_complaint',
|
|
'clinical_notes', 'is_telemedicine', 'location', 'room_number'
|
|
]
|
|
widgets = {
|
|
'provider': forms.Select(attrs={
|
|
'class': 'form-select',
|
|
'required': True
|
|
}),
|
|
'appointment_type': forms.Select(attrs={
|
|
'class': 'form-select',
|
|
'required': True
|
|
}),
|
|
'specialty': forms.Select(attrs={
|
|
'class': 'form-select',
|
|
'required': True
|
|
}),
|
|
'preferred_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date',
|
|
'required': True
|
|
}),
|
|
'preferred_time': forms.TimeInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'time',
|
|
'required': True
|
|
}),
|
|
'duration_minutes': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': '15',
|
|
'step': '15',
|
|
'value': '30'
|
|
}),
|
|
'priority': forms.Select(attrs={'class': 'form-select'}),
|
|
'chief_complaint': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Describe the reason for this appointment...',
|
|
'required': True
|
|
}),
|
|
'clinical_notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Additional clinical information (optional)'
|
|
}),
|
|
'is_telemedicine': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'location': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Building, Floor, Room (if in-person)'
|
|
}),
|
|
'room_number': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Room number (if applicable)'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'provider': 'Select the healthcare provider for this appointment',
|
|
'appointment_type': 'Type of appointment (consultation, follow-up, etc.)',
|
|
'specialty': 'Medical specialty for this appointment',
|
|
'preferred_date': 'Preferred date for the appointment',
|
|
'preferred_time': 'Preferred time for the appointment',
|
|
'duration_minutes': 'Expected duration in minutes (default: 30 minutes)',
|
|
'priority': 'Urgency level of this appointment',
|
|
'chief_complaint': 'Main reason for the appointment',
|
|
'is_telemedicine': 'Check if this is a virtual/telemedicine appointment',
|
|
'location': 'Physical location (required for in-person appointments)',
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Scope provider list to tenant
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['provider'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True,
|
|
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
|
).order_by('last_name', 'first_name')
|
|
|
|
# Set minimum date to today
|
|
self.fields['preferred_date'].widget.attrs['min'] = timezone.now().date().isoformat()
|
|
|
|
def clean_preferred_date(self):
|
|
"""Validate preferred date is not in the past."""
|
|
preferred_date = self.cleaned_data.get('preferred_date')
|
|
if preferred_date and preferred_date < timezone.now().date():
|
|
raise ValidationError(
|
|
'Appointment cannot be scheduled in the past. '
|
|
'Please select today or a future date.'
|
|
)
|
|
return preferred_date
|
|
|
|
def clean_duration_minutes(self):
|
|
"""Validate appointment duration is within acceptable range."""
|
|
duration = self.cleaned_data.get('duration_minutes')
|
|
if duration and duration < 15:
|
|
raise ValidationError(
|
|
'Appointment duration must be at least 15 minutes. '
|
|
'Please increase the duration.'
|
|
)
|
|
if duration and duration > 480: # 8 hours
|
|
raise ValidationError(
|
|
'Appointment duration cannot exceed 8 hours (480 minutes). '
|
|
'Please reduce the duration or split into multiple appointments.'
|
|
)
|
|
return duration
|
|
|
|
def clean(self):
|
|
"""Cross-field validation for appointment request."""
|
|
cleaned_data = super().clean()
|
|
is_telemedicine = cleaned_data.get('is_telemedicine')
|
|
location = cleaned_data.get('location')
|
|
preferred_date = cleaned_data.get('preferred_date')
|
|
preferred_time = cleaned_data.get('preferred_time')
|
|
|
|
# Validate location for in-person appointments
|
|
if not is_telemedicine and not location:
|
|
raise ValidationError({
|
|
'location': 'Location is required for in-person appointments. '
|
|
'Please specify the building, floor, or room.'
|
|
})
|
|
|
|
# Validate appointment is not too far in the future (e.g., 1 year)
|
|
if preferred_date:
|
|
max_future_date = timezone.now().date() + timedelta(days=365)
|
|
if preferred_date > max_future_date:
|
|
raise ValidationError({
|
|
'preferred_date': 'Appointments cannot be scheduled more than 1 year in advance. '
|
|
'Please select an earlier date.'
|
|
})
|
|
|
|
# Validate business hours (8 AM to 6 PM)
|
|
if preferred_time:
|
|
if preferred_time < time(8, 0) or preferred_time >= time(18, 0):
|
|
self.add_error('preferred_time',
|
|
'Appointments should be scheduled during business hours (8:00 AM - 6:00 PM). '
|
|
'For after-hours appointments, please contact the scheduling office.'
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class SlotAvailabilityForm(forms.ModelForm):
|
|
"""
|
|
Form for slot availability management.
|
|
"""
|
|
class Meta:
|
|
model = SlotAvailability
|
|
fields = [
|
|
'provider', 'date', 'start_time', 'end_time', 'duration_minutes',
|
|
'max_appointments', 'location', 'room_number', 'specialty',
|
|
'availability_type', 'is_active', 'supports_telemedicine'
|
|
]
|
|
widgets = {
|
|
'provider': forms.Select(attrs={'class': 'form-select'}),
|
|
'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
|
'start_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
|
|
'end_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
|
|
'duration_minutes': forms.NumberInput(attrs={'class': 'form-control', 'min': '15', 'step': '15'}),
|
|
'max_appointments': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
|
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'room_number': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'specialty': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'availability_type': forms.Select(attrs={'class': 'form-select'}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'supports_telemedicine': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['provider'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True,
|
|
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
|
).order_by('last_name', 'first_name')
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
date = cleaned_data.get('date')
|
|
start_time = cleaned_data.get('start_time')
|
|
end_time = cleaned_data.get('end_time')
|
|
|
|
if date and date < timezone.now().date():
|
|
raise ValidationError('Availability date cannot be in the past.')
|
|
|
|
if start_time and end_time and start_time >= end_time:
|
|
raise ValidationError('Start time must be before end time.')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class WaitingQueueForm(forms.ModelForm):
|
|
"""
|
|
Form for waiting queue management.
|
|
"""
|
|
# Additional fields for providers (many-to-many)
|
|
providers = forms.ModelMultipleChoiceField(
|
|
queryset=User.objects.none(),
|
|
required=False,
|
|
widget=forms.SelectMultiple(attrs={
|
|
'class': 'form-select',
|
|
'multiple': 'multiple',
|
|
'data-placeholder': 'Select providers...'
|
|
}),
|
|
help_text='Select providers assigned to this queue'
|
|
)
|
|
|
|
# Additional field for accepting patients status
|
|
is_accepting_patients = forms.BooleanField(
|
|
required=False,
|
|
initial=True,
|
|
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
help_text='Queue is accepting new patients'
|
|
)
|
|
|
|
class Meta:
|
|
model = WaitingQueue
|
|
fields = [
|
|
'name', 'description', 'queue_type', 'location', 'specialty',
|
|
'max_queue_size', 'average_service_time_minutes', 'is_active',
|
|
'is_accepting_patients'
|
|
]
|
|
widgets = {
|
|
'name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter queue name',
|
|
'required': True
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Describe the purpose and scope of this queue'
|
|
}),
|
|
'queue_type': forms.Select(attrs={
|
|
'class': 'form-select',
|
|
'required': True
|
|
}),
|
|
'location': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'e.g., Building A, Floor 2, Room 201'
|
|
}),
|
|
'specialty': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'e.g., Cardiology, Pediatrics'
|
|
}),
|
|
'max_queue_size': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': '1',
|
|
'max': '200',
|
|
'placeholder': '50',
|
|
'required': True
|
|
}),
|
|
'average_service_time_minutes': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': '1',
|
|
'max': '480',
|
|
'placeholder': '30',
|
|
'required': True
|
|
}),
|
|
'is_active': forms.CheckboxInput(attrs={
|
|
'class': 'form-check-input'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'name': 'Unique name for this queue',
|
|
'description': 'Detailed description of queue purpose and scope',
|
|
'queue_type': 'Type of queue (provider, specialty, location, etc.)',
|
|
'location': 'Physical location of the queue',
|
|
'specialty': 'Medical specialty served by this queue',
|
|
'max_queue_size': 'Maximum number of patients allowed in queue (1-200)',
|
|
'average_service_time_minutes': 'Average time per patient in minutes (1-480)',
|
|
'is_active': 'Queue is currently active and accepting patients',
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Set up providers queryset
|
|
if user and hasattr(user, 'tenant'):
|
|
tenant = user.tenant
|
|
|
|
if tenant:
|
|
self.fields['providers'].queryset = User.objects.filter(
|
|
tenant=tenant,
|
|
is_active=True,
|
|
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'SURGEON', 'PHARMACIST', 'LAB_TECH', 'RADIOLOGIST', 'THERAPIST']
|
|
).order_by('last_name', 'first_name')
|
|
|
|
# If editing existing queue, set initial providers
|
|
if self.instance and self.instance.pk:
|
|
self.fields['providers'].initial = self.instance.providers.all()
|
|
|
|
# Load existing operating hours and priority weights
|
|
if self.instance.operating_hours:
|
|
self.initial_operating_hours = self.instance.operating_hours
|
|
if self.instance.priority_weights:
|
|
self.initial_priority_weights = self.instance.priority_weights
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
# Validate max queue size
|
|
max_queue_size = cleaned_data.get('max_queue_size')
|
|
if max_queue_size and max_queue_size < 1:
|
|
raise ValidationError('Maximum queue size must be at least 1.')
|
|
|
|
# Validate average service time
|
|
avg_service_time = cleaned_data.get('average_service_time_minutes')
|
|
if avg_service_time and avg_service_time < 1:
|
|
raise ValidationError('Average service time must be at least 1 minute.')
|
|
|
|
return cleaned_data
|
|
|
|
def save(self, commit=True):
|
|
instance = super().save(commit=False)
|
|
|
|
# Handle operating hours from POST data
|
|
if hasattr(self, 'operating_hours_data'):
|
|
instance.operating_hours = self.operating_hours_data
|
|
|
|
# Handle priority weights from POST data
|
|
if hasattr(self, 'priority_weights_data'):
|
|
instance.priority_weights = self.priority_weights_data
|
|
|
|
if commit:
|
|
instance.save()
|
|
# Save many-to-many relationships
|
|
if 'providers' in self.cleaned_data:
|
|
instance.providers.set(self.cleaned_data['providers'])
|
|
self.save_m2m()
|
|
|
|
return instance
|
|
|
|
|
|
class QueueEntryForm(forms.ModelForm):
|
|
"""
|
|
Form for queue entry management.
|
|
"""
|
|
class Meta:
|
|
model = QueueEntry
|
|
fields = [
|
|
'queue', 'patient', 'appointment', 'priority_score', 'assigned_provider',
|
|
'status', 'notes'
|
|
]
|
|
widgets = {
|
|
'queue': forms.Select(attrs={'class': 'form-select'}),
|
|
'patient': forms.Select(attrs={'class': 'form-select'}),
|
|
'appointment': forms.Select(attrs={'class': 'form-select'}),
|
|
'priority_score': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
|
|
'assigned_provider': forms.Select(attrs={'class': 'form-select'}),
|
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
|
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['queue'].queryset = WaitingQueue.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True
|
|
).order_by('name')
|
|
|
|
self.fields['patient'].queryset = PatientProfile.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True
|
|
).order_by('last_name', 'first_name')
|
|
|
|
self.fields['appointment'].queryset = AppointmentRequest.objects.filter(
|
|
tenant=user.tenant,
|
|
status__in=['SCHEDULED', 'CONFIRMED']
|
|
).order_by('-preferred_date')
|
|
|
|
|
|
class TelemedicineSessionForm(forms.ModelForm):
|
|
"""
|
|
Form for telemedicine session management.
|
|
"""
|
|
class Meta:
|
|
model = TelemedicineSession
|
|
fields = [
|
|
'appointment', 'platform', 'meeting_url', 'meeting_id', 'meeting_password',
|
|
'scheduled_start', 'scheduled_end', 'status', 'waiting_room_enabled',
|
|
'recording_enabled', 'recording_consent'
|
|
]
|
|
widgets = {
|
|
'appointment': forms.Select(attrs={'class': 'form-select'}),
|
|
'platform': forms.Select(attrs={'class': 'form-select'}),
|
|
'meeting_url': forms.URLInput(attrs={'class': 'form-control'}),
|
|
'meeting_id': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'meeting_password': forms.PasswordInput(attrs={'class': 'form-control'}),
|
|
'scheduled_start': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
|
'scheduled_end': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
|
'waiting_room_enabled': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'recording_enabled': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'recording_consent': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['appointment'].queryset = AppointmentRequest.objects.filter(
|
|
tenant=user.tenant,
|
|
is_telemedicine=True,
|
|
status__in=['SCHEDULED', 'CONFIRMED', 'IN_PROGRESS']
|
|
).order_by('-preferred_date')
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
start_time = cleaned_data.get('session_start_time')
|
|
end_time = cleaned_data.get('session_end_time')
|
|
|
|
if start_time and end_time and start_time >= end_time:
|
|
raise ValidationError('Start time must be before end time.')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AppointmentTemplateForm(forms.ModelForm):
|
|
"""
|
|
Form for appointment template management.
|
|
"""
|
|
class Meta:
|
|
model = AppointmentTemplate
|
|
fields = [
|
|
'name', 'description', 'appointment_type', 'duration_minutes',
|
|
'specialty', 'advance_booking_days', 'minimum_notice_hours',
|
|
'pre_appointment_instructions', 'post_appointment_instructions',
|
|
'is_active'
|
|
]
|
|
widgets = {
|
|
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'appointment_type': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'duration_minutes': forms.NumberInput(attrs={'class': 'form-control', 'min': '15', 'step': '15'}),
|
|
'specialty': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'advance_booking_days': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
|
'minimum_notice_hours': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
|
'pre_appointment_instructions': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
|
|
'post_appointment_instructions': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# if user and hasattr(user, 'tenant'):
|
|
# self.fields['provider'].queryset = User.objects.filter(
|
|
# tenant=user.tenant,
|
|
# is_active=True,
|
|
# employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
|
# ).order_by('last_name', 'first_name')
|
|
|
|
|
|
class AppointmentSearchForm(forms.Form):
|
|
"""
|
|
Form for searching appointment data.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=255,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Search appointments, patients, providers...'
|
|
})
|
|
)
|
|
appointment_type = forms.ChoiceField(
|
|
choices=[('', 'All Types')] + list(AppointmentRequest._meta.get_field('appointment_type').choices),
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
status = forms.ChoiceField(
|
|
choices=[('', 'All Status')] + list(AppointmentRequest._meta.get_field('status').choices),
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
priority = forms.ChoiceField(
|
|
choices=[('', 'All Priorities')] + list(AppointmentRequest._meta.get_field('priority').choices),
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
provider = forms.ModelChoiceField(
|
|
queryset=User.objects.none(),
|
|
required=False,
|
|
empty_label='All Providers',
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
specialty = forms.CharField(
|
|
max_length=100,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Specialty'
|
|
})
|
|
)
|
|
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'
|
|
})
|
|
)
|
|
is_telemedicine = forms.ChoiceField(
|
|
choices=[
|
|
('', 'All Appointments'),
|
|
('true', 'Telemedicine Only'),
|
|
('false', 'In-Person Only')
|
|
],
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['provider'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True,
|
|
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
|
).order_by('last_name', 'first_name')
|
|
|
|
|
|
class QueueSearchForm(forms.Form):
|
|
"""
|
|
Form for searching queues.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=200,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Search queues...'
|
|
})
|
|
)
|
|
queue_type = forms.ChoiceField(
|
|
choices=[('', 'All Types')] + [
|
|
('PROVIDER', 'Provider Queue'),
|
|
('SPECIALTY', 'Specialty Queue'),
|
|
('LOCATION', 'Location Queue'),
|
|
('PROCEDURE', 'Procedure Queue'),
|
|
('EMERGENCY', 'Emergency Queue'),
|
|
],
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
is_active = forms.ChoiceField(
|
|
choices=[('', 'All'), ('true', 'Active'), ('false', 'Inactive')],
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
|
|
|
|
class SlotSearchForm(forms.Form):
|
|
"""
|
|
Form for searching appointment slots.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=200,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Search slots...'
|
|
})
|
|
)
|
|
provider = forms.ModelChoiceField(
|
|
queryset=User.objects.none(),
|
|
required=False,
|
|
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'
|
|
})
|
|
)
|
|
is_available = forms.ChoiceField(
|
|
choices=[('', 'All'), ('true', 'Available'), ('false', 'Booked')],
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-select'})
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user:
|
|
self.fields['provider'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
|
).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 form-select-sm',
|
|
'required': True
|
|
}),
|
|
'contact_outcome': forms.Select(attrs={
|
|
'class': 'form-select form-select-sm',
|
|
'required': True
|
|
}),
|
|
'appointment_offered': forms.CheckboxInput(attrs={
|
|
'class': 'form-check-input'
|
|
}),
|
|
'offered_date': forms.DateInput(attrs={
|
|
'class': 'form-control form-control-sm',
|
|
'type': 'date'
|
|
}),
|
|
'offered_time': forms.TimeInput(attrs={
|
|
'class': 'form-control form-control-sm',
|
|
'type': 'time'
|
|
}),
|
|
'patient_response': forms.Select(attrs={
|
|
'class': 'form-select form-select-sm'
|
|
}),
|
|
'notes': forms.Textarea(attrs={
|
|
'class': 'form-control form-control-sm',
|
|
'rows': 4,
|
|
'placeholder': 'Notes from contact attempt...'
|
|
}),
|
|
'next_contact_date': forms.DateInput(attrs={
|
|
'class': 'form-control form-control-sm',
|
|
'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
|
|
)
|
|
|
|
|