""" Forms for EMR app. """ from django import forms from django.utils import timezone from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from hr.models import Employee from .models import ( Encounter, VitalSigns, ProblemList, CarePlan, ClinicalNote, NoteTemplate ) User = get_user_model() class EncounterForm(forms.ModelForm): """ Form for creating and updating encounters. """ class Meta: model = Encounter fields = [ 'patient', 'provider', 'encounter_type', 'encounter_class', 'start_datetime', 'end_datetime', 'status', 'location', 'room_number', 'appointment', 'admission', 'chief_complaint', 'reason_for_visit', 'priority', 'acuity_level', 'billable' ] widgets = { 'start_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'end_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'chief_complaint': forms.Textarea(attrs={'rows': 3}), 'reason_for_visit': forms.Textarea(attrs={'rows': 3}), } def __init__(self, *args, **kwargs): self.tenant = kwargs.pop('tenant', None) super().__init__(*args, **kwargs) # Filter related objects by tenant if self.tenant: self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant) self.fields['provider'].queryset = User.objects.filter(tenant=self.tenant) self.fields['appointment'].queryset = self.fields['appointment'].queryset.filter(tenant=self.tenant) self.fields['admission'].queryset = self.fields['admission'].queryset.filter(tenant=self.tenant) def clean(self): cleaned_data = super().clean() start_datetime = cleaned_data.get('start_datetime') end_datetime = cleaned_data.get('end_datetime') status = cleaned_data.get('status') # Validate end_datetime is after start_datetime if start_datetime and end_datetime and end_datetime < start_datetime: self.add_error('end_datetime', 'End time must be after start time') # Validate status and end_datetime consistency if status == 'FINISHED' and not end_datetime: cleaned_data['end_datetime'] = timezone.now() return cleaned_data class VitalSignsForm(forms.ModelForm): """ Form for recording vital signs. """ class Meta: model = VitalSigns # Do NOT use both fields and exclude. Keep only fields. fields = [ 'measured_datetime', 'temperature', 'temperature_method', 'systolic_bp', 'diastolic_bp', 'bp_position', 'bp_cuff_size', 'heart_rate', 'heart_rhythm', 'respiratory_rate', 'oxygen_saturation', 'oxygen_delivery', 'oxygen_flow_rate', 'pain_scale', 'pain_location', 'pain_quality', 'weight', 'height', 'head_circumference', 'device_used', 'device_calibrated', 'notes' ] widgets = { 'measured_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'notes': forms.Textarea(attrs={'rows': 3}), 'pain_location': forms.TextInput(), 'pain_quality': forms.TextInput(), } def __init__(self, *args, **kwargs): self.tenant = kwargs.pop('tenant', None) self.user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # If you had tenant-scoped lookups for fields that ARE in the form # (e.g., device_used is FK and tenant-scoped), filter them here, e.g.: # if self.tenant and 'device_used' in self.fields: # self.fields['device_used'].queryset = Device.objects.filter(tenant=self.tenant) # DO NOT touch encounter/patient/measured_by here because they are not in the form def clean(self): cleaned_data = super().clean() systolic_bp = cleaned_data.get('systolic_bp') diastolic_bp = cleaned_data.get('diastolic_bp') if (systolic_bp and not diastolic_bp) or (diastolic_bp and not systolic_bp): self.add_error('systolic_bp', 'Both systolic and diastolic values must be provided') self.add_error('diastolic_bp', 'Both systolic and diastolic values must be provided') if systolic_bp and diastolic_bp and systolic_bp <= diastolic_bp: self.add_error('systolic_bp', 'Systolic pressure must be greater than diastolic pressure') self.add_error('diastolic_bp', 'Systolic pressure must be greater than diastolic pressure') oxygen_delivery = cleaned_data.get('oxygen_delivery') oxygen_flow_rate = cleaned_data.get('oxygen_flow_rate') # Ensure the constant matches your choices exactly ROOM_AIR = 'ROOM_AIR' # change to 'room_air' if that’s what your choices use if oxygen_delivery and oxygen_delivery != ROOM_AIR and not oxygen_flow_rate: self.add_error('oxygen_flow_rate', 'Flow rate is required for this oxygen delivery method') return cleaned_data class ProblemListForm(forms.ModelForm): """ Form for creating and updating problems. """ # Custom field for ICD-10 search icd10_search = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'class': 'form-control form-control-sm', 'placeholder': 'Search ICD-10 diagnoses...', 'id': 'icd10-search', 'autocomplete': 'off' }), help_text='Start typing to search ICD-10 diagnoses' ) class Meta: model = ProblemList fields = [ 'patient', 'problem_name', 'problem_code', 'coding_system', 'problem_type', 'onset_date', 'onset_description', 'severity', 'priority', 'status', 'resolution_date', 'resolution_notes', 'diagnosing_provider', 'managing_provider', 'related_encounter', 'body_site', 'laterality', 'clinical_notes', 'patient_concerns', 'treatment_goals', 'outcome_measures', 'verified', 'verified_by' ] widgets = { 'patient': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'related_encounter': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'problem_name': forms.TextInput(attrs={ 'class': 'form-control form-control-sm', 'placeholder': 'Problem name (auto-filled from ICD-10)', 'readonly': True }), 'problem_code': forms.TextInput(attrs={ 'class': 'form-control form-control-sm', 'placeholder': 'ICD-10 code (auto-filled)', 'readonly': True }), 'coding_system': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'problem_type': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'onset_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}), 'onset_description': forms.TextInput(attrs={'class': 'form-control form-control-sm', 'placeholder': 'Describe onset'}), 'severity': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'priority': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'status': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'resolution_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}), 'body_site': forms.TextInput(attrs={'class': 'form-control form-control-sm', 'placeholder': 'Body site affected'}), 'laterality': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'diagnosing_provider': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'managing_provider': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'verified_by': forms.Select(attrs={'class': 'form-select form-select-sm'}), 'clinical_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm', 'placeholder': 'Clinical notes'}), 'patient_concerns': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm', 'placeholder': 'Patient concerns'}), 'resolution_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm', 'placeholder': 'Resolution notes'}), 'treatment_goals': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm', 'placeholder': 'Treatment goals'}), 'outcome_measures': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm', 'placeholder': 'Outcome measures'}), 'verified': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } def __init__(self, *args, **kwargs): self.tenant = kwargs.pop('tenant', None) super().__init__(*args, **kwargs) # Filter related objects by tenant if self.tenant: self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant) self.fields['diagnosing_provider'].queryset = User.objects.filter(tenant=self.tenant) self.fields['managing_provider'].queryset = User.objects.filter(tenant=self.tenant) self.fields['verified_by'].queryset = User.objects.filter(tenant=self.tenant) self.fields['related_encounter'].queryset = Encounter.objects.filter(tenant=self.tenant) def clean(self): cleaned_data = super().clean() status = cleaned_data.get('status') resolution_date = cleaned_data.get('resolution_date') # Validate resolution date is provided for resolved problems if status in ['RESOLVED', 'REMISSION'] and not resolution_date: self.add_error('resolution_date', 'Resolution date is required for resolved problems') # Validate resolution date is not in the future if resolution_date and resolution_date > timezone.now().date(): self.add_error('resolution_date', 'Resolution date cannot be in the future') # Validate onset date is not in the future onset_date = cleaned_data.get('onset_date') if onset_date and onset_date > timezone.now().date(): self.add_error('onset_date', 'Onset date cannot be in the future') # Validate resolution date is after onset date if onset_date and resolution_date and resolution_date < onset_date: self.add_error('resolution_date', 'Resolution date must be after onset date') return cleaned_data class CarePlanForm(forms.ModelForm): """ Form for creating and updating care plans. """ class Meta: model = CarePlan fields = [ 'patient', 'title', 'description', 'plan_type', 'category', 'start_date', 'end_date', 'target_completion_date', 'status', 'priority', 'primary_provider', 'care_team', 'related_problems', 'goals', 'objectives', 'interventions', 'activities', 'monitoring_parameters', 'evaluation_criteria', 'patient_goals', 'patient_preferences', 'patient_barriers', 'resources_needed', 'support_systems', 'progress_notes', 'last_reviewed', 'next_review_date', 'outcomes_achieved', 'completion_percentage', 'approved', 'approved_by' ] widgets = { 'plan_type': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'category': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'patient': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'title': forms.TextInput(attrs={'class': 'form-control form-control-sm'}), 'priority': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'status': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'primary_provider': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'care_team': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'completion_percentage': forms.NumberInput(attrs={'class': 'form-control form-control-sm'}), 'approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'approved_by': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'related_problems': forms.SelectMultiple(attrs={'class': 'form-control form-control-sm'}), 'start_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}), 'end_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}), 'target_completion_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}), 'last_reviewed': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}), 'next_review_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}), 'description': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'patient_goals': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'patient_preferences': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'patient_barriers': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'progress_notes': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'goals': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'objectives': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'interventions': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'activities': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'monitoring_parameters': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'evaluation_criteria': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'resources_needed': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'support_systems': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), 'outcomes_achieved': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}), } def __init__(self, *args, **kwargs): self.tenant = kwargs.pop('tenant', None) super().__init__(*args, **kwargs) # Filter related objects by tenant if self.tenant: self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant) self.fields['primary_provider'].queryset = User.objects.filter(tenant=self.tenant) self.fields['care_team'].queryset = User.objects.filter(tenant=self.tenant) self.fields['approved_by'].queryset = User.objects.filter(tenant=self.tenant) self.fields['related_problems'].queryset = ProblemList.objects.filter(tenant=self.tenant) def clean(self): cleaned_data = super().clean() start_date = cleaned_data.get('start_date') end_date = cleaned_data.get('end_date') target_completion_date = cleaned_data.get('target_completion_date') # Validate end_date is after start_date if start_date and end_date and end_date < start_date: self.add_error('end_date', 'End date must be after start date') # Validate target_completion_date is after start_date if start_date and target_completion_date and target_completion_date < start_date: self.add_error('target_completion_date', 'Target completion date must be after start date') # Validate completion percentage completion_percentage = cleaned_data.get('completion_percentage') status = cleaned_data.get('status') if status == 'COMPLETED' and completion_percentage < 100: self.add_error('completion_percentage', 'Completion percentage must be 100% for completed care plans') if status == 'DRAFT' and completion_percentage > 0: self.add_error('completion_percentage', 'Completion percentage must be 0% for draft care plans') return cleaned_data class CarePlanProgressForm(forms.ModelForm): """ Restricted form used to update progress on an existing care plan without touching identity/scope fields (patient, goals, etc.). """ class Meta: model = CarePlan fields = [ 'status', 'completion_percentage', 'progress_notes', 'outcomes_achieved', 'evaluation_criteria', 'monitoring_parameters', 'last_reviewed', 'next_review_date', ] widgets = { 'status': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'completion_percentage': forms.NumberInput(attrs={ 'class': 'form-control form-control-sm', 'min': 0, 'max': 100, 'step': 1, }), 'progress_notes': forms.Textarea(attrs={'rows': 4, 'class': 'form-control form-control-sm'}), 'outcomes_achieved': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}), 'evaluation_criteria': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}), 'monitoring_parameters': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}), 'last_reviewed': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}), 'next_review_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}), } def __init__(self, *args, **kwargs): # Accept tenant for potential future use; not strictly needed here. self.tenant = kwargs.pop('tenant', None) super().__init__(*args, **kwargs) # Sensible default: prefill last_reviewed with today if empty if not self.instance.pk and not self.fields['last_reviewed'].initial: self.fields['last_reviewed'].initial = timezone.now().date() def clean(self): cleaned = super().clean() status = cleaned.get('status') pct = cleaned.get('completion_percentage') # Range check if pct is not None and not (0 <= pct <= 100): self.add_error('completion_percentage', 'Completion must be between 0 and 100.') # Status / % consistency if status == 'COMPLETED' and (pct is None or pct < 100): self.add_error('completion_percentage', 'Completion must be 100% when status is COMPLETED.') if status == 'DRAFT' and pct and pct > 0: self.add_error('completion_percentage', 'Draft care plans must have 0% completion.') # Dates sanity: next_review_date should not be before last_reviewed last_rev = cleaned.get('last_reviewed') next_rev = cleaned.get('next_review_date') if last_rev and next_rev and next_rev < last_rev: self.add_error('next_review_date', 'Next review date must be on/after Last reviewed.') return cleaned class ClinicalNoteForm(forms.ModelForm): """ Form for creating and updating clinical notes. """ class Meta: model = ClinicalNote fields = [ 'note_type', 'title', 'content', 'template', 'structured_data', 'co_signers', 'status', 'note_datetime', 'electronically_signed', 'signed_datetime', 'signature_method', 'amended_note', 'amendment_reason', 'confidential', 'restricted_access', 'access_restrictions', 'related_problems', 'related_care_plans' ] widgets = { 'note_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'signed_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'content': forms.Textarea(attrs={'rows': 10, 'class': 'note-editor'}), 'amendment_reason': forms.Textarea(attrs={'rows': 3}), 'structured_data': forms.Textarea(attrs={'rows': 3}), 'access_restrictions': forms.Textarea(attrs={'rows': 3}), } def __init__(self, *args, **kwargs): self.tenant = kwargs.pop('tenant', None) self.user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter related objects by tenant if self.tenant: # self.fields['encounter'].queryset = Encounter.objects.filter(tenant=self.tenant) # self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant) # self.fields['author'].queryset = User.objects.filter(tenant=self.tenant) self.fields['co_signers'].queryset = User.objects.filter(tenant=self.tenant) self.fields['template'].queryset = NoteTemplate.objects.filter(tenant=self.tenant, is_active=True) self.fields['amended_note'].queryset = ClinicalNote.objects.filter( patient__tenant=self.tenant ).exclude(pk=self.instance.pk if self.instance.pk else None) self.fields['related_problems'].queryset = ProblemList.objects.filter(tenant=self.tenant) self.fields['related_care_plans'].queryset = CarePlan.objects.filter(tenant=self.tenant) def clean(self): cleaned_data = super().clean() status = cleaned_data.get('status') electronically_signed = cleaned_data.get('electronically_signed') signed_datetime = cleaned_data.get('signed_datetime') # Validate signature information if status == 'SIGNED' and not electronically_signed: self.add_error('electronically_signed', 'Note must be electronically signed if status is Signed') if electronically_signed and not signed_datetime: cleaned_data['signed_datetime'] = timezone.now() # Validate amendment information amended_note = cleaned_data.get('amended_note') amendment_reason = cleaned_data.get('amendment_reason') if amended_note and not amendment_reason: self.add_error('amendment_reason', 'Amendment reason is required when amending a note') return cleaned_data class NoteTemplateForm(forms.ModelForm): """ Form for creating and updating note templates. """ class Meta: model = NoteTemplate fields = [ 'name', 'description', 'note_type', 'specialty', 'template_content', 'structured_fields', 'is_active', 'is_default', 'version', 'previous_version', 'quality_indicators', 'compliance_requirements' ] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control form-control-sm'}), 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}), 'note_type': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'template_content': forms.Textarea(attrs={'rows': 10, 'class': 'form-control form-control-sm'}), 'structured_fields': forms.Textarea(attrs={'rows': 5, 'class': 'form-control form-control-sm'}), 'quality_indicators': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}), 'compliance_requirements': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}), 'version': forms.TextInput(attrs={'class': 'form-control form-control-sm'}), 'previous_version': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_default': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'specialty': forms.Select(attrs={'class': 'form-control form-control-sm'}), } def __init__(self, *args, **kwargs): self.tenant = kwargs.pop('tenant', None) self.user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter related objects by tenant if self.tenant: self.fields['previous_version'].queryset = NoteTemplate.objects.filter( tenant=self.tenant ).exclude(pk=self.instance.pk if self.instance.pk else None) def clean(self): cleaned_data = super().clean() is_default = cleaned_data.get('is_default') note_type = cleaned_data.get('note_type') specialty = cleaned_data.get('specialty') # Validate template content template_content = cleaned_data.get('template_content') if not template_content or len(template_content.strip()) < 10: self.add_error('template_content', 'Template content is too short') # Validate version format version = cleaned_data.get('version') if version: import re if not re.match(r'^\d+\.\d+$', version): self.add_error('version', 'Version must be in format X.Y (e.g., 1.0)') return cleaned_data