agdar/nursing/forms.py
2025-11-02 14:35:35 +03:00

275 lines
12 KiB
Python

"""
Nursing forms for the Tenhal Multidisciplinary Healthcare Platform.
This module contains forms for nursing encounters, vitals, and growth charts.
Based on MD-N-F-1 form.
"""
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 NursingEncounter, GrowthChart
class NursingEncounterForm(forms.ModelForm):
"""
Form for nursing encounter (MD-N-F-1).
Auto-calculates BMI from height and weight.
"""
class Meta:
model = NursingEncounter
fields = [
'patient', 'appointment', 'encounter_date', 'filled_by',
'height_cm', 'weight_kg', 'head_circumference_cm',
'hr_bpm', 'bp_systolic', 'bp_diastolic',
'respiratory_rate', 'spo2', 'crt',
'pain_score', 'temperature',
'allergy_present', 'allergy_details',
'observations',
]
widgets = {
'encounter_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'filled_by': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select nurse'}),
'height_cm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
'weight_kg': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
'head_circumference_cm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
'hr_bpm': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'bp_systolic': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'bp_diastolic': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'respiratory_rate': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'spo2': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
'crt': forms.Select(attrs={'class': 'form-control'}),
'pain_score': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '10'}),
'temperature': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'allergy_present': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'allergy_details': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}),
'observations': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'patient': forms.HiddenInput(),
'appointment': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'patient',
'appointment',
Fieldset(
_('Encounter Information'),
Row(
Column('encounter_date', css_class='form-group col-md-6 mb-0'),
Column('filled_by', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
),
Fieldset(
_('Anthropometric Measurements'),
Row(
Column('height_cm', css_class='form-group col-md-4 mb-0'),
Column('weight_kg', css_class='form-group col-md-4 mb-0'),
Column('head_circumference_cm', css_class='form-group col-md-4 mb-0'),
css_class='form-row'
),
HTML('''
<div class="alert alert-info" id="bmi-display">
<strong>{% trans "BMI:" %}</strong> <span id="bmi-value">--</span>
</div>
'''),
),
Fieldset(
_('Vital Signs'),
Row(
Column('hr_bpm', css_class='form-group col-md-3 mb-0'),
Column('respiratory_rate', css_class='form-group col-md-3 mb-0'),
Column('spo2', css_class='form-group col-md-3 mb-0'),
Column('temperature', css_class='form-group col-md-3 mb-0'),
css_class='form-row'
),
Row(
Column('bp_systolic', css_class='form-group col-md-4 mb-0'),
Column('bp_diastolic', css_class='form-group col-md-4 mb-0'),
Column('crt', css_class='form-group col-md-4 mb-0'),
css_class='form-row'
),
'pain_score',
),
Fieldset(
_('Allergies'),
'allergy_present',
'allergy_details',
),
Fieldset(
_('Clinical Notes'),
'observations',
),
HTML('''
<script>
// Auto-calculate BMI
function calculateBMI() {
const height = parseFloat(document.querySelector('[name="height_cm"]').value);
const weight = parseFloat(document.querySelector('[name="weight_kg"]').value);
if (height && weight && height > 0) {
const heightM = height / 100;
const bmi = (weight / (heightM * heightM)).toFixed(2);
document.getElementById('bmi-value').textContent = bmi;
} else {
document.getElementById('bmi-value').textContent = '--';
}
}
document.addEventListener('DOMContentLoaded', function() {
const heightInput = document.querySelector('[name="height_cm"]');
const weightInput = document.querySelector('[name="weight_kg"]');
if (heightInput && weightInput) {
heightInput.addEventListener('input', calculateBMI);
weightInput.addEventListener('input', calculateBMI);
calculateBMI(); // Initial calculation
}
});
</script>
'''),
Submit('submit', _('Save Nursing Encounter'), css_class='btn btn-primary')
)
def clean(self):
cleaned_data = super().clean()
allergy_present = cleaned_data.get('allergy_present')
allergy_details = cleaned_data.get('allergy_details')
# Validate allergy details if allergy_present is True
if allergy_present and not allergy_details:
raise forms.ValidationError(
_('Please provide allergy details when allergies are present.')
)
# Validate vital signs ranges
hr_bpm = cleaned_data.get('hr_bpm')
if hr_bpm and (hr_bpm < 30 or hr_bpm > 250):
self.add_error('hr_bpm', _('Heart rate seems unusual. Please verify.'))
bp_systolic = cleaned_data.get('bp_systolic')
bp_diastolic = cleaned_data.get('bp_diastolic')
if bp_systolic and bp_diastolic and bp_systolic <= bp_diastolic:
raise forms.ValidationError(
_('Systolic blood pressure must be higher than diastolic.')
)
spo2 = cleaned_data.get('spo2')
if spo2 and (spo2 < 50 or spo2 > 100):
self.add_error('spo2', _('SpO2 value seems unusual. Please verify.'))
return cleaned_data
class GrowthChartForm(forms.ModelForm):
"""
Form for recording growth chart data.
"""
class Meta:
model = GrowthChart
fields = [
'patient', 'measurement_date', 'age_months',
'height_cm', 'weight_kg', 'head_circumference_cm',
'percentile_height', 'percentile_weight', 'percentile_head_circumference',
]
widgets = {
'patient': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select patient'}),
'measurement_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'age_months': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'height_cm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
'weight_kg': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
'head_circumference_cm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}),
'percentile_height': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
'percentile_weight': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
'percentile_head_circumference': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'patient',
Row(
Column('measurement_date', css_class='form-group col-md-6 mb-0'),
Column('age_months', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Fieldset(
_('Measurements'),
Row(
Column('height_cm', css_class='form-group col-md-4 mb-0'),
Column('weight_kg', css_class='form-group col-md-4 mb-0'),
Column('head_circumference_cm', css_class='form-group col-md-4 mb-0'),
css_class='form-row'
),
),
Fieldset(
_('Percentiles'),
Row(
Column('percentile_height', css_class='form-group col-md-4 mb-0'),
Column('percentile_weight', css_class='form-group col-md-4 mb-0'),
Column('percentile_head_circumference', css_class='form-group col-md-4 mb-0'),
css_class='form-row'
),
HTML('''
<div class="alert alert-info">
<small>{% trans "Percentiles are calculated based on WHO growth standards." %}</small>
</div>
'''),
),
Submit('submit', _('Save Growth Chart Entry'), css_class='btn btn-primary')
)
class NursingEncounterSearchForm(forms.Form):
"""
Form for searching nursing encounters.
"""
search_query = forms.CharField(
required=False,
label=_('Search'),
widget=forms.TextInput(attrs={
'placeholder': _('Patient name, MRN...'),
'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'})
)
has_alerts = forms.BooleanField(
required=False,
label=_('Has Vital Signs Alerts'),
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'get'
self.helper.layout = Layout(
Row(
Column('search_query', css_class='form-group col-md-5 mb-0'),
Column('date_from', css_class='form-group col-md-3 mb-0'),
Column('date_to', css_class='form-group col-md-3 mb-0'),
Column('has_alerts', css_class='form-group col-md-1 mb-0'),
css_class='form-row'
),
Submit('search', _('Search'), css_class='btn btn-primary')
)