""" 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 ( SignedDocumentEditPreventionMixin, 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! This document can no longer be edited.") 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': {}, }