Marwan Alwali a4665842c9 update
2025-11-23 10:58:07 +03:00

640 lines
23 KiB
Python

"""
Appointments forms for the Tenhal Multidisciplinary Healthcare Platform.
This module contains forms for appointment booking, confirmation, rescheduling, etc.
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Row, Column, Submit, HTML
from .models import Appointment, Provider, Room, Schedule
class AppointmentBookingForm(forms.ModelForm):
"""
Form for booking new appointments.
"""
# Define common service types
SERVICE_TYPE_CHOICES = [
('', _('Select a service type')),
('consultation', _('Consultation')),
('follow_up', _('Follow-up Visit')),
('assessment', _('Assessment')),
('therapy_session', _('Therapy Session')),
('evaluation', _('Evaluation')),
('intervention', _('Intervention')),
('screening', _('Screening')),
('parent_training', _('Parent Training')),
('group_session', _('Group Session')),
('other', _('Other')),
]
# Appointment type selection
APPOINTMENT_TYPE_CHOICES = [
('single', _('Single Session')),
('package', _('Use Package')),
]
appointment_type = forms.ChoiceField(
choices=APPOINTMENT_TYPE_CHOICES,
widget=forms.RadioSelect(attrs={'class': 'form-check-input'}),
label=_('Appointment Type'),
initial='single',
required=False
)
# Package selection (shown when appointment_type = 'package')
package_purchase = forms.ModelChoiceField(
queryset=None, # Will be set in __init__
required=False,
label=_('Select Package'),
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select a package'}),
help_text=_('Select an active package with remaining sessions')
)
service_type = forms.ChoiceField(
choices=SERVICE_TYPE_CHOICES,
widget=forms.Select(attrs={'class': 'form-control'}),
label=_('Service Type')
)
class Meta:
model = Appointment
fields = [
'patient', 'clinic', 'provider', 'room',
'service_type', 'scheduled_date', 'scheduled_time', 'duration',
'finance_cleared', 'consent_verified',
'notes',
]
widgets = {
'patient': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select patient'}),
'clinic': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select clinic'}),
'provider': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'}),
'room': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select room'}),
'scheduled_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'scheduled_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'duration': forms.NumberInput(attrs={'class': 'form-control', 'min': '15', 'step': '15'}),
'finance_cleared': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'consent_verified': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
patient = kwargs.pop('patient', None)
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Package selection will be handled via modal
# Set empty queryset initially - will be populated dynamically
from finance.models import PackagePurchase
self.fields['package_purchase'].queryset = PackagePurchase.objects.none()
# Store patient and tenant for later use
self.patient = patient
self.tenant = tenant
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Fieldset(
_('Appointment Type'),
'appointment_type',
),
Fieldset(
_('Package Selection'),
'package_purchase',
css_id='packageSection',
css_class='d-none'
),
Fieldset(
_('Appointment Details'),
'patient',
Row(
Column('clinic', css_class='form-group col-md-6 mb-0'),
Column('provider', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Row(
Column('service_type', css_class='form-group col-md-6 mb-0'),
Column('room', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Row(
Column('scheduled_date', css_class='form-group col-md-4 mb-0'),
Column('scheduled_time', css_class='form-group col-md-4 mb-0'),
Column('duration', css_class='form-group col-md-4 mb-0'),
css_class='form-row'
),
'notes',
),
Submit('submit', _('Book Appointment'), css_class='btn btn-primary')
)
def clean(self):
cleaned_data = super().clean()
provider = cleaned_data.get('provider')
clinic = cleaned_data.get('clinic')
# Validate provider belongs to clinic
if provider and clinic:
if not provider.specialties.filter(id=clinic.id).exists():
raise forms.ValidationError(
_('Selected provider does not work in this clinic.')
)
return cleaned_data
class AppointmentConfirmForm(forms.Form):
"""
Form for confirming appointments.
"""
confirmation_method = forms.ChoiceField(
label=_('Confirmation Method'),
choices=Appointment.ConfirmationMethod.choices,
widget=forms.RadioSelect(attrs={'class': 'form-check-input'})
)
notes = forms.CharField(
required=False,
label=_('Notes'),
widget=forms.Textarea(attrs={'rows': 2, 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'confirmation_method',
'notes',
Submit('submit', _('Confirm Appointment'), css_class='btn btn-success')
)
class AppointmentRescheduleForm(forms.ModelForm):
"""
Form for rescheduling appointments.
"""
reschedule_reason = forms.CharField(
label=_('Reason for Rescheduling'),
widget=forms.Textarea(attrs={'rows': 3, 'class': 'form-control'})
)
class Meta:
model = Appointment
fields = ['scheduled_date', 'scheduled_time', 'reschedule_reason']
widgets = {
'scheduled_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'scheduled_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
}
labels = {
'scheduled_date': _('New Date'),
'scheduled_time': _('New Time'),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Row(
Column('scheduled_date', css_class='form-group col-md-6 mb-0'),
Column('scheduled_time', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
'reschedule_reason',
Submit('submit', _('Reschedule'), css_class='btn btn-warning')
)
class AppointmentCancelForm(forms.Form):
"""
Form for cancelling appointments.
"""
cancel_reason = forms.CharField(
label=_('Reason for Cancellation'),
widget=forms.Textarea(attrs={'rows': 3, 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'cancel_reason',
Submit('submit', _('Cancel Appointment'), css_class='btn btn-danger')
)
class AppointmentArrivalForm(forms.Form):
"""
Form for marking patient arrival.
"""
notes = forms.CharField(
required=False,
label=_('Notes'),
widget=forms.Textarea(attrs={'rows': 2, 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'notes',
Submit('submit', _('Mark as Arrived'), css_class='btn btn-info')
)
class AppointmentSearchForm(forms.Form):
"""
Form for searching appointments.
"""
search_query = forms.CharField(
required=False,
label=_('Search'),
widget=forms.TextInput(attrs={
'placeholder': _('Patient name, MRN, Appointment #...'),
'class': 'form-control'
})
)
clinic = forms.ModelChoiceField(
required=False,
label=_('Clinic'),
queryset=None, # Will be set in __init__
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select clinic'})
)
provider = forms.ModelChoiceField(
required=False,
label=_('Provider'),
queryset=None, # Will be set in __init__
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'})
)
status = forms.ChoiceField(
required=False,
label=_('Status'),
choices=[('', _('All'))] + list(Appointment.Status.choices),
widget=forms.Select(attrs={'class': 'form-control'})
)
date_from = forms.DateField(
required=False,
label=_('From Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
date_to = forms.DateField(
required=False,
label=_('To Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
from core.models import Clinic
self.fields['clinic'].queryset = Clinic.objects.filter(tenant=tenant, is_active=True)
self.fields['provider'].queryset = Provider.objects.filter(tenant=tenant, is_available=True)
self.helper = FormHelper()
self.helper.form_method = 'get'
self.helper.layout = Layout(
Row(
Column('search_query', css_class='form-group col-md-3 mb-0'),
Column('clinic', css_class='form-group col-md-2 mb-0'),
Column('provider', css_class='form-group col-md-2 mb-0'),
Column('status', css_class='form-group col-md-2 mb-0'),
Column('date_from', css_class='form-group col-md-1.5 mb-0'),
Column('date_to', css_class='form-group col-md-1.5 mb-0'),
css_class='form-row'
),
Submit('search', _('Search'), css_class='btn btn-primary')
)
class ProviderScheduleForm(forms.ModelForm):
"""
Form for managing provider schedules.
"""
class Meta:
model = Schedule
fields = ['provider', 'day_of_week', 'start_time', 'end_time', 'slot_duration', 'is_active']
widgets = {
'provider': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'}),
'day_of_week': forms.Select(attrs={'class': 'form-control'}),
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'slot_duration': forms.NumberInput(attrs={'class': 'form-control', 'min': '15', 'step': '15'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'provider',
Row(
Column('day_of_week', css_class='form-group col-md-4 mb-0'),
Column('start_time', css_class='form-group col-md-3 mb-0'),
Column('end_time', css_class='form-group col-md-3 mb-0'),
Column('slot_duration', css_class='form-group col-md-2 mb-0'),
css_class='form-row'
),
'is_active',
Submit('submit', _('Save Schedule'), css_class='btn btn-primary')
)
def clean(self):
cleaned_data = super().clean()
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
if start_time and end_time and start_time >= end_time:
raise forms.ValidationError(_('End time must be after start time.'))
return cleaned_data
# Alias for backward compatibility with views
AppointmentForm = AppointmentBookingForm
RescheduleForm = AppointmentRescheduleForm
# ============================================================================
# Session Forms (Group Session Support)
# ============================================================================
class GroupSessionCreateForm(forms.ModelForm):
"""
Form for creating a new group session.
"""
SERVICE_TYPE_CHOICES = [
('', _('Select a service type')),
('group_therapy', _('Group Therapy')),
('group_assessment', _('Group Assessment')),
('parent_training', _('Parent Training Group')),
('social_skills', _('Social Skills Group')),
('behavior_management', _('Behavior Management Group')),
('other', _('Other')),
]
service_type = forms.ChoiceField(
choices=SERVICE_TYPE_CHOICES,
widget=forms.Select(attrs={'class': 'form-control'}),
label=_('Service Type')
)
class Meta:
from .models import Session
model = Session
fields = [
'provider', 'clinic', 'room', 'service_type',
'scheduled_date', 'scheduled_time', 'duration',
'max_capacity', 'group_notes'
]
widgets = {
'provider': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'}),
'clinic': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select clinic'}),
'room': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select room'}),
'scheduled_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'scheduled_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'duration': forms.NumberInput(attrs={'class': 'form-control', 'min': '15', 'step': '15', 'value': '60'}),
'max_capacity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1', 'max': '20', 'value': '8'}),
'group_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': 'Shared notes for the entire group session'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Fieldset(
_('Group Session Details'),
Row(
Column('provider', css_class='form-group col-md-6 mb-0'),
Column('clinic', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Row(
Column('service_type', css_class='form-group col-md-6 mb-0'),
Column('room', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Row(
Column('scheduled_date', css_class='form-group col-md-4 mb-0'),
Column('scheduled_time', css_class='form-group col-md-4 mb-0'),
Column('duration', css_class='form-group col-md-4 mb-0'),
css_class='form-row'
),
Row(
Column('max_capacity', css_class='form-group col-md-12 mb-0'),
css_class='form-row'
),
'group_notes',
),
Submit('submit', _('Create Group Session'), css_class='btn btn-primary')
)
def clean_max_capacity(self):
"""Validate capacity is between 1 and 20."""
capacity = self.cleaned_data.get('max_capacity')
if capacity and (capacity < 1 or capacity > 20):
raise forms.ValidationError(_('Capacity must be between 1 and 20 patients'))
return capacity
class AddPatientToSessionForm(forms.Form):
"""
Form for adding a patient to an existing session.
"""
patient = forms.ModelChoiceField(
queryset=None, # Will be set in __init__
label=_('Patient'),
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select patient'})
)
individual_notes = forms.CharField(
required=False,
label=_('Individual Notes'),
widget=forms.Textarea(attrs={'rows': 2, 'class': 'form-control', 'placeholder': 'Notes specific to this patient'})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
session = kwargs.pop('session', None)
super().__init__(*args, **kwargs)
if tenant:
from core.models import Patient
# Get patients not already in this session
queryset = Patient.objects.filter(tenant=tenant)
if session:
# Exclude patients already enrolled
enrolled_patient_ids = session.participants.filter(
status__in=['BOOKED', 'CONFIRMED', 'ARRIVED', 'ATTENDED']
).values_list('patient_id', flat=True)
queryset = queryset.exclude(id__in=enrolled_patient_ids)
self.fields['patient'].queryset = queryset
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'patient',
'individual_notes',
Submit('submit', _('Add Patient to Session'), css_class='btn btn-success')
)
class SessionParticipantStatusForm(forms.Form):
"""
Form for updating participant status.
"""
STATUS_CHOICES = [
('CONFIRMED', _('Confirm')),
('ARRIVED', _('Mark as Arrived')),
('ATTENDED', _('Mark as Attended')),
('NO_SHOW', _('Mark as No-Show')),
('CANCELLED', _('Cancel')),
]
status = forms.ChoiceField(
choices=STATUS_CHOICES,
label=_('Action'),
widget=forms.Select(attrs={'class': 'form-control'})
)
notes = forms.CharField(
required=False,
label=_('Notes'),
widget=forms.Textarea(attrs={'rows': 2, 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'status',
'notes',
Submit('submit', _('Update Status'), css_class='btn btn-primary')
)
class SessionSearchForm(forms.Form):
"""
Form for searching sessions.
"""
search_query = forms.CharField(
required=False,
label=_('Search'),
widget=forms.TextInput(attrs={
'placeholder': _('Session number, provider name...'),
'class': 'form-control'
})
)
session_type = forms.ChoiceField(
required=False,
label=_('Session Type'),
choices=[('', _('All'))] + [('INDIVIDUAL', _('Individual')), ('GROUP', _('Group'))],
widget=forms.Select(attrs={'class': 'form-control'})
)
clinic = forms.ModelChoiceField(
required=False,
label=_('Clinic'),
queryset=None, # Will be set in __init__
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select clinic'})
)
provider = forms.ModelChoiceField(
required=False,
label=_('Provider'),
queryset=None, # Will be set in __init__
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'})
)
status = forms.ChoiceField(
required=False,
label=_('Status'),
choices=[('', _('All')), ('SCHEDULED', _('Scheduled')), ('IN_PROGRESS', _('In Progress')),
('COMPLETED', _('Completed')), ('CANCELLED', _('Cancelled'))],
widget=forms.Select(attrs={'class': 'form-control'})
)
date_from = forms.DateField(
required=False,
label=_('From Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
date_to = forms.DateField(
required=False,
label=_('To Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
from core.models import Clinic
self.fields['clinic'].queryset = Clinic.objects.filter(tenant=tenant, is_active=True)
self.fields['provider'].queryset = Provider.objects.filter(tenant=tenant, is_available=True)
self.helper = FormHelper()
self.helper.form_method = 'get'
self.helper.layout = Layout(
Row(
Column('search_query', css_class='form-group col-md-3 mb-0'),
Column('session_type', css_class='form-group col-md-2 mb-0'),
Column('clinic', css_class='form-group col-md-2 mb-0'),
Column('provider', css_class='form-group col-md-2 mb-0'),
Column('status', css_class='form-group col-md-1 mb-0'),
Column('date_from', css_class='form-group col-md-1 mb-0'),
Column('date_to', css_class='form-group col-md-1 mb-0'),
css_class='form-row'
),
Submit('search', _('Search'), css_class='btn btn-primary')
)
class GroupSessionNotesForm(forms.ModelForm):
"""
Form for editing group session notes.
"""
class Meta:
from .models import Session
model = Session
fields = ['group_notes']
widgets = {
'group_notes': forms.Textarea(attrs={'rows': 5, 'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'group_notes',
Submit('submit', _('Save Notes'), css_class='btn btn-primary')
)