275 lines
12 KiB
Python
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')
|
|
)
|