agdar/nursing/views.py
Marwan Alwali a04817ef6e update
2025-11-02 19:25:08 +03:00

525 lines
19 KiB
Python

"""
Nursing views for the Tenhal Multidisciplinary Healthcare Platform.
This module contains views for nursing documentation including:
- Nursing encounters (vitals, anthropometrics)
- Growth charts
- Allergy tracking
- Observation notes
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import JsonResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
from django.urls import reverse_lazy
from django.contrib import messages
from core.mixins import (
TenantFilterMixin,
RolePermissionMixin,
AuditLogMixin,
HTMXResponseMixin,
SuccessMessageMixin,
PaginationMixin,
)
from core.models import User, Patient
from appointments.models import Appointment
from .models import NursingEncounter
from .forms import NursingEncounterForm
# Sign View
class NursingEncounterSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
"""Sign a nursing encounter."""
allowed_roles = [User.Role.ADMIN, User.Role.NURSE]
def post(self, request, pk):
encounter = get_object_or_404(NursingEncounter, pk=pk, tenant=request.user.tenant)
if encounter.signed_by:
messages.warning(request, "This encounter has already been signed.")
return HttpResponseRedirect(reverse_lazy('nursing:encounter_detail', kwargs={'pk': pk}))
if encounter.filled_by != request.user and request.user.role != User.Role.ADMIN:
messages.error(request, "Only the nurse who filled this encounter or an administrator can sign it.")
return HttpResponseRedirect(reverse_lazy('nursing:encounter_detail', kwargs={'pk': pk}))
encounter.signed_by = request.user
encounter.signed_at = timezone.now()
encounter.save()
messages.success(request, "Encounter signed successfully!")
return HttpResponseRedirect(reverse_lazy('nursing:encounter_detail', kwargs={'pk': pk}))
class NursingEncounterListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
HTMXResponseMixin, ListView):
"""
Nursing encounter list view.
Features:
- Filter by patient, date range
- Search by patient name/MRN
- Role-based filtering (nurses see their encounters)
- Quick vitals summary
"""
model = NursingEncounter
template_name = 'nursing/encounter_list.html'
htmx_template_name = 'nursing/partials/encounter_list_partial.html'
context_object_name = 'encounters'
paginate_by = 25
def get_queryset(self):
"""Get filtered queryset."""
queryset = super().get_queryset()
user = self.request.user
# Role-based filtering
if user.role == User.Role.NURSE:
queryset = queryset.filter(filled_by=user)
# Apply search
search_query = self.request.GET.get('search', '').strip()
if search_query:
queryset = queryset.filter(
Q(patient__first_name_en__icontains=search_query) |
Q(patient__last_name_en__icontains=search_query) |
Q(patient__mrn__icontains=search_query)
)
# Apply filters
patient_id = self.request.GET.get('patient')
if patient_id:
queryset = queryset.filter(patient_id=patient_id)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(encounter_date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(encounter_date__lte=date_to)
return queryset.select_related('patient', 'filled_by', 'appointment', 'signed_by').order_by('-encounter_date')
def get_context_data(self, **kwargs):
"""Add filter options and unsigned counts."""
context = super().get_context_data(**kwargs)
user = self.request.user
# Get unsigned encounters
unsigned_query = NursingEncounter.objects.filter(tenant=user.tenant, signed_by__isnull=True)
if user.role == User.Role.NURSE:
unsigned_query = unsigned_query.filter(filled_by=user)
context['unsigned_count'] = unsigned_query.count()
context['unsigned_items'] = unsigned_query.select_related('patient', 'filled_by').order_by('-encounter_date')[:10]
# Add current filters
context['current_filters'] = {
'search': self.request.GET.get('search', ''),
'patient': self.request.GET.get('patient', ''),
'date_from': self.request.GET.get('date_from', ''),
'date_to': self.request.GET.get('date_to', ''),
}
return context
class NursingEncounterDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
"""
Nursing encounter detail view.
Features:
- Full vitals and anthropometrics
- BMI calculation and interpretation
- Growth chart visualization
- Allergy information
- Observation notes
"""
model = NursingEncounter
template_name = 'nursing/encounter_detail.html'
context_object_name = 'encounter'
def get_context_data(self, **kwargs):
"""Add calculated values and interpretations."""
context = super().get_context_data(**kwargs)
encounter = self.object
# BMI interpretation
if encounter.bmi:
context['bmi_interpretation'] = self._interpret_bmi(encounter.bmi)
# Vital signs interpretation
context['vitals_interpretation'] = self._interpret_vitals(encounter)
# Get previous encounters for comparison
context['previous_encounters'] = NursingEncounter.objects.filter(
patient=encounter.patient,
encounter_date__lt=encounter.encounter_date
).order_by('-encounter_date')[:5]
# Growth chart data (for visualization)
context['growth_data'] = self._get_growth_chart_data(encounter.patient)
return context
def _interpret_bmi(self, bmi):
"""Interpret BMI value."""
if bmi < 18.5:
return {'category': 'Underweight', 'color': 'warning'}
elif 18.5 <= bmi < 25:
return {'category': 'Normal weight', 'color': 'success'}
elif 25 <= bmi < 30:
return {'category': 'Overweight', 'color': 'warning'}
else:
return {'category': 'Obese', 'color': 'danger'}
def _interpret_vitals(self, encounter):
"""Interpret vital signs."""
interpretations = {}
# Heart rate
if encounter.hr_bpm:
if encounter.hr_bpm < 60:
interpretations['hr'] = {'status': 'Low', 'color': 'warning'}
elif 60 <= encounter.hr_bpm <= 100:
interpretations['hr'] = {'status': 'Normal', 'color': 'success'}
else:
interpretations['hr'] = {'status': 'High', 'color': 'danger'}
# Blood pressure
if encounter.bp_systolic and encounter.bp_diastolic:
if encounter.bp_systolic < 120 and encounter.bp_diastolic < 80:
interpretations['bp'] = {'status': 'Normal', 'color': 'success'}
elif encounter.bp_systolic < 140 and encounter.bp_diastolic < 90:
interpretations['bp'] = {'status': 'Elevated', 'color': 'warning'}
else:
interpretations['bp'] = {'status': 'High', 'color': 'danger'}
# SpO2
if encounter.spo2:
if encounter.spo2 >= 95:
interpretations['spo2'] = {'status': 'Normal', 'color': 'success'}
elif encounter.spo2 >= 90:
interpretations['spo2'] = {'status': 'Low', 'color': 'warning'}
else:
interpretations['spo2'] = {'status': 'Critical', 'color': 'danger'}
return interpretations
def _get_growth_chart_data(self, patient):
"""Get growth chart data for patient."""
encounters = NursingEncounter.objects.filter(
patient=patient
).order_by('encounter_date')
data = {
'dates': [],
'weight': [],
'height': [],
'head_circ': [],
'bmi': [],
}
for encounter in encounters:
data['dates'].append(encounter.encounter_date.strftime('%Y-%m-%d'))
data['weight'].append(float(encounter.weight_kg) if encounter.weight_kg else None)
data['height'].append(float(encounter.height_cm) if encounter.height_cm else None)
data['head_circ'].append(float(encounter.head_circumference_cm) if encounter.head_circumference_cm else None)
data['bmi'].append(float(encounter.bmi) if encounter.bmi else None)
return data
class NursingEncounterCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin,
SuccessMessageMixin, CreateView):
"""
Nursing encounter creation view.
Features:
- Record vitals and anthropometrics
- Auto-calculate BMI
- Link to appointment
- Allergy documentation
"""
model = NursingEncounter
form_class = NursingEncounterForm
template_name = 'nursing/encounter_form.html'
success_message = "Nursing encounter recorded successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.NURSE]
def get_success_url(self):
"""Redirect to encounter detail."""
return reverse_lazy('nursing:encounter_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
"""Set tenant and filled_by."""
# Set tenant
form.instance.tenant = self.request.user.tenant
# Set filled_by
form.instance.filled_by = self.request.user
# Set encounter date if not provided
if not form.instance.encounter_date:
form.instance.encounter_date = timezone.now().date()
# Calculate BMI if height and weight provided
if form.instance.height_cm and form.instance.weight_kg:
height_m = form.instance.height_cm / 100
form.instance.bmi = form.instance.weight_kg / (height_m ** 2)
# Save encounter
response = super().form_valid(form)
# Update appointment status if linked
if self.object.appointment:
appointment = self.object.appointment
if appointment.status == Appointment.Status.IN_PROGRESS:
# Keep in progress, nursing is part of the visit
pass
return response
def get_context_data(self, **kwargs):
"""Add form title and patient/appointment info."""
context = super().get_context_data(**kwargs)
context['form_title'] = 'Record Nursing Encounter'
context['submit_text'] = 'Save Encounter'
# Get patient if provided
patient_id = self.request.GET.get('patient')
if patient_id:
try:
context['patient'] = Patient.objects.get(
pk=patient_id,
tenant=self.request.user.tenant
)
except Patient.DoesNotExist:
pass
# Get appointment if provided
appointment_id = self.request.GET.get('appointment_id')
if appointment_id:
try:
context['appointment'] = Appointment.objects.get(
pk=appointment_id,
tenant=self.request.user.tenant
)
context['patient'] = context['appointment'].patient
except Appointment.DoesNotExist:
pass
return context
class NursingEncounterUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
Nursing encounter update view.
Features:
- Update vitals and anthropometrics
- Recalculate BMI
- Version history
"""
model = NursingEncounter
form_class = NursingEncounterForm
template_name = 'nursing/encounter_form.html'
success_message = "Nursing encounter updated successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.NURSE]
def get_success_url(self):
"""Redirect to encounter detail."""
return reverse_lazy('nursing:encounter_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
"""Recalculate BMI if needed."""
# Recalculate BMI if height or weight changed
if form.instance.height_cm and form.instance.weight_kg:
height_m = form.instance.height_cm / 100
form.instance.bmi = form.instance.weight_kg / (height_m ** 2)
return super().form_valid(form)
def get_context_data(self, **kwargs):
"""Add form title and history."""
context = super().get_context_data(**kwargs)
context['form_title'] = f'Update Nursing Encounter - {self.object.patient.mrn}'
context['submit_text'] = 'Update Encounter'
# Add version history if available
if hasattr(self.object, 'history'):
context['history'] = self.object.history.all()[:10]
return context
class PatientVitalsHistoryView(LoginRequiredMixin, TenantFilterMixin, ListView):
"""
Patient vitals history view.
Features:
- All vitals for a specific patient
- Trend visualization
- Growth charts
- Export to PDF
"""
model = NursingEncounter
template_name = 'nursing/patient_vitals_history.html'
context_object_name = 'encounters'
paginate_by = 50
def get_queryset(self):
"""Get all encounters for patient."""
patient_id = self.kwargs.get('patient_id')
return NursingEncounter.objects.filter(
patient_id=patient_id,
tenant=self.request.user.tenant
).order_by('-encounter_date')
def get_context_data(self, **kwargs):
"""Add patient and chart data."""
context = super().get_context_data(**kwargs)
patient_id = self.kwargs.get('patient_id')
context['patient'] = get_object_or_404(
Patient,
pk=patient_id,
tenant=self.request.user.tenant
)
# Get chart data
encounters = self.get_queryset()
context['chart_data'] = self._prepare_chart_data(encounters)
# Get statistics
context['stats'] = self._calculate_statistics(encounters)
return context
def _prepare_chart_data(self, encounters):
"""Prepare data for charts."""
data = {
'dates': [],
'weight': [],
'height': [],
'bmi': [],
'hr': [],
'bp_sys': [],
'bp_dia': [],
'spo2': [],
'temperature': [],
}
for encounter in encounters.order_by('encounter_date'):
data['dates'].append(encounter.encounter_date.strftime('%Y-%m-%d'))
data['weight'].append(float(encounter.weight_kg) if encounter.weight_kg else None)
data['height'].append(float(encounter.height_cm) if encounter.height_cm else None)
data['bmi'].append(float(encounter.bmi) if encounter.bmi else None)
data['hr'].append(encounter.hr_bpm if encounter.hr_bpm else None)
data['bp_sys'].append(encounter.bp_systolic if encounter.bp_systolic else None)
data['bp_dia'].append(encounter.bp_diastolic if encounter.bp_diastolic else None)
data['spo2'].append(encounter.spo2 if encounter.spo2 else None)
data['temperature'].append(float(encounter.temperature) if encounter.temperature else None)
return data
def _calculate_statistics(self, encounters):
"""Calculate statistics from encounters."""
if not encounters:
return {}
latest = encounters.first()
oldest = encounters.last()
stats = {
'total_encounters': encounters.count(),
'latest_weight': latest.weight_kg if latest.weight_kg else None,
'latest_height': latest.height_cm if latest.height_cm else None,
'latest_bmi': latest.bmi if latest.bmi else None,
}
# Calculate weight change if we have both
if latest.weight_kg and oldest.weight_kg and encounters.count() > 1:
stats['weight_change'] = latest.weight_kg - oldest.weight_kg
# Calculate height change if we have both
if latest.height_cm and oldest.height_cm and encounters.count() > 1:
stats['height_change'] = latest.height_cm - oldest.height_cm
return stats
class GrowthChartView(LoginRequiredMixin, TenantFilterMixin, DetailView):
"""
Growth chart visualization view.
Features:
- WHO growth standards overlay
- Patient measurements plotted
- Percentile calculations
- Export to PDF
"""
model = Patient
template_name = 'nursing/growth_chart.html'
context_object_name = 'patient'
pk_url_kwarg = 'patient_id'
def get_context_data(self, **kwargs):
"""Add growth chart data."""
context = super().get_context_data(**kwargs)
patient = self.object
# Get all encounters
encounters = NursingEncounter.objects.filter(
patient=patient
).order_by('encounter_date')
# Prepare chart data
context['chart_data'] = self._prepare_growth_chart_data(encounters, patient)
# Get WHO standards (placeholder - would need actual WHO data)
context['who_standards'] = self._get_who_standards(patient)
return context
def _prepare_growth_chart_data(self, encounters, patient):
"""Prepare growth chart data."""
data = {
'age_months': [],
'weight': [],
'height': [],
'head_circ': [],
'bmi': [],
}
for encounter in encounters:
# Calculate age in months at encounter
age_months = self._calculate_age_months(patient.date_of_birth, encounter.encounter_date)
data['age_months'].append(age_months)
data['weight'].append(float(encounter.weight_kg) if encounter.weight_kg else None)
data['height'].append(float(encounter.height_cm) if encounter.height_cm else None)
data['head_circ'].append(float(encounter.head_circumference_cm) if encounter.head_circumference_cm else None)
data['bmi'].append(float(encounter.bmi) if encounter.bmi else None)
return data
def _calculate_age_months(self, birth_date, encounter_date):
"""Calculate age in months."""
years = encounter_date.year - birth_date.year
months = encounter_date.month - birth_date.month
return years * 12 + months
def _get_who_standards(self, patient):
"""Get WHO growth standards for patient's gender."""
# TODO: Implement WHO growth standards
# This would load actual WHO percentile data based on patient gender
return {
'percentiles': [3, 15, 50, 85, 97],
'weight': {},
'height': {},
'bmi': {},
}