""" Views for EMR app. """ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.http import JsonResponse, HttpResponse from django.db.models import Q, Count, Avg, F, Case, When, IntegerField, Max from django.utils.translation import gettext_lazy as _ from django.utils import timezone from django.contrib import messages from django.core.paginator import Paginator from django.template.loader import render_to_string from datetime import datetime, timedelta, date import json from django.contrib.messages.views import SuccessMessageMixin from core.models import AuditLogEntry from core.utils import AuditLogger from patients.models import PatientProfile from .models import * from .forms import * from core.mixins import TenantMixin, FormKwargsMixin class EMRDashboardView(LoginRequiredMixin, ListView): """ Main dashboard for EMR management. """ template_name = 'emr/dashboard.html' context_object_name = 'encounters' def get_queryset(self): """Get recent encounters for current tenant.""" return Encounter.objects.filter( tenant=self.request.user.tenant ).select_related('patient', 'provider').order_by('-start_datetime')[:10] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tenant = self.request.user.tenant today = timezone.now().date() # Dashboard statistics context.update({ 'total_encounters': Encounter.objects.filter(tenant=tenant).count(), 'active_encounters': Encounter.objects.filter( tenant=tenant, status__in=['ARRIVED', 'TRIAGED', 'IN_PROGRESS', 'ON_HOLD'] ).count(), 'todays_encounters': Encounter.objects.filter( tenant=tenant, start_datetime__date=today ).count(), 'pending_documentation': Encounter.objects.filter( tenant=tenant, documentation_complete=False, status='FINISHED' ).count(), 'unsigned_notes': ClinicalNote.objects.filter( patient__tenant=tenant, status='COMPLETED', electronically_signed=False ).count(), 'active_problems': ProblemList.objects.filter( tenant=tenant, status='ACTIVE' ).count(), 'active_care_plans': CarePlan.objects.filter( tenant=tenant, status='ACTIVE' ).count(), 'critical_vitals': VitalSigns.objects.filter( patient__tenant=tenant, measured_datetime__date=today ).exclude(critical_values=[]).count(), }) return context class EncounterListView(LoginRequiredMixin, ListView): """ List view for encounters. """ model = Encounter template_name = 'emr/encounter_list.html' context_object_name = 'encounters' paginate_by = 25 def get_queryset(self): queryset = Encounter.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(provider__first_name__icontains=search) | Q(provider__last_name__icontains=search) | Q(chief_complaint__icontains=search) ) # Filter by encounter type encounter_type = self.request.GET.get('encounter_type') if encounter_type: queryset = queryset.filter(encounter_type=encounter_type) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by provider provider_id = self.request.GET.get('provider') if provider_id: queryset = queryset.filter(provider_id=provider_id) # Filter by date range date_from = self.request.GET.get('date_from') date_to = self.request.GET.get('date_to') if date_from: queryset = queryset.filter(start_datetime__date__gte=date_from) if date_to: queryset = queryset.filter(start_datetime__date__lte=date_to) return queryset.select_related('patient', 'provider').order_by('-start_datetime') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'encounter_types': Encounter._meta.get_field('encounter_type').choices, 'encounter_statuses': Encounter._meta.get_field('status').choices, }) return context class EncounterCreateView(LoginRequiredMixin, CreateView): model = Encounter form_class = EncounterForm template_name = 'emr/encounter_create.html' success_message = _('Encounter for %(patient)s created successfully.') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='Encounter', object_id=str(self.object.pk), changes={'status': 'Encounter created'} ) return response def get_success_url(self): return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) class EncounterDetailView(LoginRequiredMixin, DetailView): """ Detail view for encounter. """ model = Encounter template_name = 'emr/encounter_detail.html' context_object_name = 'encounter' def get_queryset(self): return Encounter.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) encounter = self.object # Get related data context.update({ 'vital_signs': encounter.vital_signs.all().order_by('-measured_datetime'), 'clinical_notes': encounter.clinical_notes.all().order_by('-note_datetime'), 'problems': encounter.problems_identified.all().order_by('-created_at'), }) return context class VitalSignsListView(LoginRequiredMixin, ListView): """ List view for vital signs. """ model = VitalSigns template_name = 'emr/vital_signs_list.html' context_object_name = 'vital_signs' paginate_by = 25 def get_queryset(self): queryset = VitalSigns.objects.filter(patient__tenant=self.request.user.tenant) # Filter by patient patient_id = self.request.GET.get('patient') if patient_id: queryset = queryset.filter(patient_id=patient_id) # Filter by encounter encounter_id = self.request.GET.get('encounter') if encounter_id: queryset = queryset.filter(encounter_id=encounter_id) # Filter by date range date_from = self.request.GET.get('date_from') date_to = self.request.GET.get('date_to') if date_from: queryset = queryset.filter(measured_datetime__date__gte=date_from) if date_to: queryset = queryset.filter(measured_datetime__date__lte=date_to) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) ) return queryset.select_related('patient', 'encounter', 'measured_by').order_by('-measured_datetime') class ProblemListView(LoginRequiredMixin, ListView): """ List view for problem list. """ model = ProblemList template_name = 'emr/problem_list.html' context_object_name = 'problems' paginate_by = 25 def get_queryset(self): queryset = ProblemList.objects.filter(tenant=self.request.user.tenant) # Filter by patient patient_id = self.request.GET.get('patient') if patient_id: queryset = queryset.filter(patient_id=patient_id) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by problem type problem_type = self.request.GET.get('problem_type') if problem_type: queryset = queryset.filter(problem_type=problem_type) # Filter by priority priority = self.request.GET.get('priority') if priority: queryset = queryset.filter(priority=priority) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(problem_name__icontains=search) | Q(problem_code__icontains=search) ) return queryset.select_related( 'patient', 'diagnosing_provider', 'managing_provider' ).order_by('-created_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'problem_types': ProblemList._meta.get_field('problem_type').choices, 'problem_statuses': ProblemList._meta.get_field('status').choices, 'priorities': ProblemList._meta.get_field('priority').choices, }) return context class CarePlanListView(LoginRequiredMixin, ListView): """ List view for care plans. """ model = CarePlan template_name = 'emr/care_plan_list.html' context_object_name = 'care_plans' paginate_by = 25 def get_queryset(self): queryset = CarePlan.objects.filter(tenant=self.request.user.tenant) # Filter by patient patient_id = self.request.GET.get('patient') if patient_id: queryset = queryset.filter(patient_id=patient_id) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by plan type plan_type = self.request.GET.get('plan_type') if plan_type: queryset = queryset.filter(plan_type=plan_type) # Filter by provider provider_id = self.request.GET.get('provider') if provider_id: queryset = queryset.filter(primary_provider_id=provider_id) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(title__icontains=search) | Q(description__icontains=search) ) return queryset.select_related('patient', 'primary_provider').order_by('-created_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'plan_types': CarePlan._meta.get_field('plan_type').choices, 'plan_statuses': CarePlan._meta.get_field('status').choices, }) return context class ClinicalNoteListView(LoginRequiredMixin, ListView): """ List view for clinical notes. """ model = ClinicalNote template_name = 'emr/clinical_note_list.html' context_object_name = 'notes' paginate_by = 25 def get_queryset(self): queryset = ClinicalNote.objects.filter(patient__tenant=self.request.user.tenant) # Filter by patient patient_id = self.request.GET.get('patient') if patient_id: queryset = queryset.filter(patient_id=patient_id) # Filter by note type note_type = self.request.GET.get('note_type') if note_type: queryset = queryset.filter(note_type=note_type) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by author author_id = self.request.GET.get('author') if author_id: queryset = queryset.filter(author_id=author_id) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(title__icontains=search) | Q(content__icontains=search) ) return queryset.select_related('patient', 'encounter', 'author').order_by('-note_datetime') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'note_types': ClinicalNote._meta.get_field('note_type').choices, 'note_statuses': ClinicalNote._meta.get_field('status').choices, }) return context class ClinicalNoteDetailView(LoginRequiredMixin, TenantMixin, DetailView): model = ClinicalNote template_name = 'emr/clinical_note_detail.html' context_object_name = 'note' class ClinicalNoteCreateView( LoginRequiredMixin, FormKwargsMixin, SuccessMessageMixin, CreateView ): model = ClinicalNote form_class = ClinicalNoteForm template_name = 'emr/clinical_note_form.html' success_message = _('Clinical note created successfully.') def form_valid(self, form): form.instance.author = self.request.user response = super().form_valid(form) AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='ClinicalNote', object_id=str(self.object.pk), changes={'status': 'Clinical note created'} ) return response def get_success_url(self): return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) class ClinicalNoteUpdateView( LoginRequiredMixin, FormKwargsMixin, SuccessMessageMixin, TenantMixin, UpdateView ): model = ClinicalNote form_class = ClinicalNoteForm template_name = 'emr/clinical_note_form.html' success_message = _('Clinical note updated successfully.') def form_valid(self, form): response = super().form_valid(form) AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='UPDATE', model_name='ClinicalNote', object_id=str(self.object.pk), changes={'status': 'Clinical note updated'} ) return response def get_success_url(self): return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) class ClinicalNoteDeleteView( LoginRequiredMixin, TenantMixin, SuccessMessageMixin, DeleteView ): model = ClinicalNote template_name = 'emr/clinical_notes/clinical_note_confirm_delete.html' success_url = reverse_lazy('emr:clinical_note_list') success_message = _('Clinical note deleted successfully.') def delete(self, request, *args, **kwargs): cn = self.get_object() AuditLogEntry.objects.create( tenant=request.user.tenant, user=request.user, action='DELETE', model_name='ClinicalNote', object_id=str(cn.pk), changes={'status': 'Clinical note deleted'} ) messages.success(request, self.success_message) return super().delete(request, *args, **kwargs) # HTMX Views for real-time updates @login_required def emr_stats(request): """ HTMX endpoint for EMR statistics. """ tenant = request.user.tenant today = timezone.now().date() stats = { 'active_encounters': Encounter.objects.filter( tenant=tenant, status__in=['ARRIVED', 'TRIAGED', 'IN_PROGRESS', 'ON_HOLD'] ).count(), 'pending_documentation': Encounter.objects.filter( tenant=tenant, documentation_complete=False, status='FINISHED' ).count(), 'unsigned_notes': ClinicalNote.objects.filter( patient__tenant=tenant, status='COMPLETED', electronically_signed=False ).count(), 'critical_vitals': VitalSigns.objects.filter( patient__tenant=tenant, measured_datetime__date=today ).exclude(critical_values=[]).count(), } return render(request, 'emr/partials/emr_stats.html', {'stats': stats}) @login_required def encounter_search(request): """ HTMX endpoint for encounter search. """ search = request.GET.get('search', '') status = request.GET.get('status', '') encounter_type = request.GET.get('encounter_type', '') queryset = Encounter.objects.filter(tenant=request.user.tenant) if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(chief_complaint__icontains=search) ) if status: queryset = queryset.filter(status=status) if encounter_type: queryset = queryset.filter(encounter_type=encounter_type) encounters = queryset.select_related('patient', 'provider').order_by('-start_datetime')[:20] return render(request, 'emr/partials/encounter_list.html', {'encounters': encounters}) @login_required def vital_signs_chart(request, patient_id): """ HTMX endpoint for vital signs chart. """ patient = get_object_or_404( PatientProfile, id=patient_id, tenant=request.user.tenant ) # Get recent vital signs vital_signs = VitalSigns.objects.filter( patient=patient ).order_by('-measured_datetime')[:10] return render(request, 'emr/partials/vital_signs_chart.html', { 'patient': patient, 'vital_signs': vital_signs }) @login_required def problem_list_patient(request, patient_id): """ HTMX endpoint for patient problem list. """ patient = get_object_or_404( PatientProfile, id=patient_id, tenant=request.user.tenant ) problems = ProblemList.objects.filter( patient=patient ).select_related('diagnosing_provider').order_by('-created_at') return render(request, 'emr/partials/problem_list.html', { 'patient': patient, 'problems': problems }) @login_required def add_vital_signs(request, encounter_id): """ HTMX endpoint for adding vital signs. """ if request.method == 'POST': encounter = get_object_or_404( Encounter, id=encounter_id, tenant=request.user.tenant ) # Extract vital signs data vital_signs_data = { 'encounter': encounter, 'patient': encounter.patient, 'measured_by': request.user, 'measured_datetime': timezone.now(), } # Add measurements if provided if request.POST.get('temperature'): vital_signs_data['temperature'] = float(request.POST.get('temperature')) vital_signs_data['temperature_method'] = request.POST.get('temperature_method', 'ORAL') if request.POST.get('systolic_bp') and request.POST.get('diastolic_bp'): vital_signs_data['systolic_bp'] = int(request.POST.get('systolic_bp')) vital_signs_data['diastolic_bp'] = int(request.POST.get('diastolic_bp')) vital_signs_data['bp_position'] = request.POST.get('bp_position', 'SITTING') if request.POST.get('heart_rate'): vital_signs_data['heart_rate'] = int(request.POST.get('heart_rate')) vital_signs_data['heart_rhythm'] = request.POST.get('heart_rhythm', 'REGULAR') if request.POST.get('respiratory_rate'): vital_signs_data['respiratory_rate'] = int(request.POST.get('respiratory_rate')) if request.POST.get('oxygen_saturation'): vital_signs_data['oxygen_saturation'] = int(request.POST.get('oxygen_saturation')) vital_signs_data['oxygen_delivery'] = request.POST.get('oxygen_delivery', 'ROOM_AIR') if request.POST.get('pain_scale'): vital_signs_data['pain_scale'] = int(request.POST.get('pain_scale')) vital_signs_data['pain_location'] = request.POST.get('pain_location', '') if request.POST.get('weight'): vital_signs_data['weight'] = float(request.POST.get('weight')) if request.POST.get('height'): vital_signs_data['height'] = float(request.POST.get('height')) # Create vital signs record vital_signs = VitalSigns.objects.create(**vital_signs_data) # Log the action AuditLogger.log_event( user=request.user, action='VITAL_SIGNS_RECORDED', model='VitalSigns', object_id=vital_signs.id, details=f"Vital signs recorded for {encounter.patient.get_full_name()}" ) messages.success(request, 'Vital signs recorded successfully') return JsonResponse({'success': True, 'vital_signs_id': vital_signs.id}) return JsonResponse({'error': 'Invalid request'}, status=400) @login_required def add_problem(request, patient_id): """ HTMX endpoint for adding a problem. """ if request.method == 'POST': patient = get_object_or_404( PatientProfile, id=patient_id, tenant=request.user.tenant ) problem_data = { 'tenant': request.user.tenant, 'patient': patient, 'problem_name': request.POST.get('problem_name'), 'problem_type': request.POST.get('problem_type', 'DIAGNOSIS'), 'severity': request.POST.get('severity'), 'priority': request.POST.get('priority', 'MEDIUM'), 'diagnosing_provider': request.user, 'clinical_notes': request.POST.get('clinical_notes', ''), } # Add optional fields if request.POST.get('problem_code'): problem_data['problem_code'] = request.POST.get('problem_code') problem_data['coding_system'] = request.POST.get('coding_system', 'ICD10') if request.POST.get('onset_date'): problem_data['onset_date'] = request.POST.get('onset_date') # Create problem problem = ProblemList.objects.create(**problem_data) # Log the action AuditLogger.log_event( user=request.user, action='PROBLEM_ADDED', model='ProblemList', object_id=problem.id, details=f"Problem added for {patient.get_full_name()}: {problem.problem_name}" ) messages.success(request, f'Problem "{problem.problem_name}" added successfully') return JsonResponse({'success': True, 'problem_id': problem.id}) return JsonResponse({'error': 'Invalid request'}, status=400) @login_required def update_encounter_status(request, encounter_id): """ HTMX endpoint for updating encounter status. """ if request.method == 'POST': encounter = get_object_or_404( Encounter, id=encounter_id, tenant=request.user.tenant ) new_status = request.POST.get('status') if new_status in dict(Encounter._meta.get_field('status').choices): old_status = encounter.status encounter.status = new_status # Handle status-specific logic if new_status == 'FINISHED': encounter.end_datetime = timezone.now() elif new_status == 'IN_PROGRESS' and not encounter.end_datetime: encounter.end_datetime = None encounter.save() # Log the action AuditLogger.log_event( user=request.user, action='ENCOUNTER_STATUS_UPDATED', model='Encounter', object_id=encounter.id, details=f"Encounter status changed from {old_status} to {new_status}" ) return JsonResponse({ 'success': True, 'new_status': encounter.get_status_display(), 'status_class': get_status_class(new_status) }) return JsonResponse({'error': 'Invalid request'}, status=400) @login_required def sign_note(request, note_id): """ HTMX endpoint for signing a clinical note. """ if request.method == 'POST': note = get_object_or_404( ClinicalNote, id=note_id, patient__tenant=request.user.tenant, author=request.user, status='COMPLETED' ) note.status = 'SIGNED' note.electronically_signed = True note.signed_datetime = timezone.now() note.signature_method = 'ELECTRONIC' note.save() # Log the action AuditLogger.log_event( user=request.user, action='NOTE_SIGNED', model='ClinicalNote', object_id=note.id, details=f"Clinical note signed: {note.title}" ) messages.success(request, f'Note "{note.title}" signed successfully') return JsonResponse({'success': True}) return JsonResponse({'error': 'Invalid request'}, status=400) def get_status_class(status): """ Get CSS class for status display. """ status_classes = { 'PLANNED': 'secondary', 'ARRIVED': 'info', 'TRIAGED': 'warning', 'IN_PROGRESS': 'primary', 'ON_HOLD': 'warning', 'FINISHED': 'success', 'CANCELLED': 'danger', 'ENTERED_IN_ERROR': 'danger', 'UNKNOWN': 'secondary', } return status_classes.get(status, 'secondary') # # # encounters/views.py # # from django.shortcuts import get_object_or_404, redirect, render # from django.urls import reverse_lazy # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin # from django.contrib import messages # from django.views.generic import ( # TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.views.generic.edit import FormMixin # from django.contrib.messages.views import SuccessMessageMixin # from django.db.models import Q, Avg # from django.utils import timezone # from django.http import JsonResponse # # from .models import * # from .forms import * # from core.models import AuditLogEntry # from patients.models import PatientProfile # from django.utils.translation import gettext_lazy as _ # # # # Mixins ------------------------------------------------------------------------- # # class TenantMixin: # def get_queryset(self): # qs = super().get_queryset() # tenant = getattr(self.request.user, 'tenant', None) # if tenant and not self.request.user.is_superuser: # # Models with patient FK: # if hasattr(qs.model, 'patient'): # return qs.filter(patient__tenant=tenant) # # NoteTemplate uses tenant directly: # return qs.filter(tenant=tenant) # return qs # # def get_object(self, queryset=None): # qs = queryset or self.get_queryset() # return super().get_object(qs) # # # class FormKwargsMixin: # def get_form_kwargs(self): # kw = super().get_form_kwargs() # kw['user'] = self.request.user # kw['tenant'] = getattr(self.request.user, 'tenant', None) # return kw # # # # Dashboard ---------------------------------------------------------------------- # # class DashboardView(LoginRequiredMixin, TemplateView): # template_name = 'emr/dashboard.html' # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # tenant = getattr(self.request.user, 'tenant', None) # today = timezone.now().date() # week_ago = today - timezone.timedelta(days=7) # # enc = Encounter.objects.filter(patient__tenant=tenant) # vs = VitalSigns.objects.filter(encounter__patient__tenant=tenant) # pb = ProblemList.objects.filter(patient__tenant=tenant) # cp = CarePlan.objects.filter(patient__tenant=tenant) # cn = ClinicalNote.objects.filter(encounter__patient__tenant=tenant) # # ctx.update({ # 'total_encounters': enc.count(), # 'encounters_today': enc.filter(scheduled_datetime__date=today).count(), # 'encounters_this_week': enc.filter(scheduled_datetime__date__gte=week_ago).count(), # 'active_encounters': enc.filter(status='IN_PROGRESS').count(), # # 'total_vital_signs': vs.count(), # 'vital_signs_today': vs.filter(recorded_at__date=today).count(), # 'avg_temp_week': vs.filter(recorded_at__date__gte=week_ago).aggregate(Avg('temperature'))['temperature__avg'], # 'avg_hr_week': vs.filter(recorded_at__date__gte=week_ago).aggregate(Avg('heart_rate'))['heart_rate__avg'], # # 'total_problems': pb.count(), # 'active_problems': pb.filter(status='ACTIVE').count(), # 'resolved_problems': pb.filter(status='RESOLVED').count(), # # 'total_care_plans': cp.count(), # 'active_care_plans': cp.filter(status='ACTIVE').count(), # 'completed_care_plans': cp.filter(status='COMPLETED').count(), # # 'total_notes': cn.count(), # 'notes_today': cn.filter(created_at__date=today).count(), # # 'recent_encounters': enc.select_related('patient','provider').order_by('-scheduled_datetime')[:5], # 'recent_vitals': vs.select_related('encounter','recorded_by').order_by('-recorded_at')[:5], # 'recent_notes': cn.select_related('encounter','author').order_by('-created_at')[:5], # }) # return ctx # # # # Encounter ---------------------------------------------------------------------- # # class EncounterListView(LoginRequiredMixin, TenantMixin, FormMixin, ListView): # model = Encounter # template_name = 'emr/encounter_list.html' # context_object_name = 'encounters' # paginate_by = 20 # form_class = EMRSearchForm # # def get_queryset(self): # qs = super().get_queryset().select_related('patient','provider').order_by('-scheduled_datetime') # if self.request.GET: # form = self.get_form() # if form.is_valid(): # cd = form.cleaned_data # if cd.get('search'): # qs = qs.filter( # Q(patient__first_name__icontains=cd['search']) | # Q(patient__last_name__icontains=cd['search']) | # Q(chief_complaint__icontains=cd['search']) # ) # for fld in ('patient','provider','encounter_type','status'): # if cd.get(fld): # qs = qs.filter(**{fld: cd[fld]}) # if cd.get('date_from'): # qs = qs.filter(scheduled_datetime__date__gte=cd['date_from']) # if cd.get('date_to'): # qs = qs.filter(scheduled_datetime__date__lte=cd['date_to']) # return qs # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # ctx['search_form'] = self.get_form() # ctx['total_count'] = self.get_queryset().count() # return ctx # # # class EncounterDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = Encounter # template_name = 'emr/encounter_detail.html' # context_object_name = 'encounter' # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # enc = self.object # ctx.update({ # 'vital_signs': VitalSigns.objects.filter(encounter=enc).order_by('-recorded_at'), # 'clinical_notes': ClinicalNote.objects.filter(encounter=enc).order_by('-created_at'), # 'problems': ProblemList.objects.filter(patient=enc.patient, status='ACTIVE'), # 'care_plans': CarePlan.objects.filter(patient=enc.patient, status='ACTIVE'), # }) # return ctx # # # class EncounterCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = Encounter # form_class = EncounterForm # template_name = 'emr/encounter_form.html' # success_message = _('Encounter for %(patient)s created successfully.') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='Encounter', # object_id=str(self.object.pk), # changes={'status': 'Encounter created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) # # # class EncounterUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = Encounter # form_class = EncounterForm # template_name = 'emr/encounter_form.html' # success_message = _('Encounter for %(patient)s updated successfully.') # # def dispatch(self, request, *args, **kwargs): # enc = self.get_object() # if enc.status == 'COMPLETED' and not request.user.is_superuser: # messages.error(request, _('Cannot modify a completed encounter.')) # return redirect('emr:encounter_detail', pk=enc.pk) # return super().dispatch(request, *args, **kwargs) # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='Encounter', # object_id=str(self.object.pk), # changes={'status': 'Encounter updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) # # # class EncounterDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = Encounter # template_name = 'emr/encounter_confirm_delete.html' # success_url = reverse_lazy('emr:encounter_list') # success_message = _('Encounter deleted successfully.') # # def delete(self, request, *args, **kwargs): # enc = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='Encounter', # object_id=str(enc.pk), # changes={'status': 'Encounter deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # VitalSigns ---------------------------------------------------------------------- # # class VitalSignsListView(LoginRequiredMixin, TenantMixin, ListView): # model = VitalSigns # template_name = 'emr/vital_signs_list.html' # context_object_name = 'vital_signs' # paginate_by = 20 # # def get_queryset(self): # qs = super().get_queryset().select_related('encounter','recorded_by').order_by('-recorded_at') # # (Search/filter logic would use a VitalSignsSearchForm — omitted for brevity) # return qs # # # class VitalSignsDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = VitalSigns # template_name = 'emr/vital_signs_detail.html' # context_object_name = 'vital_signs' # # # class VitalSignsCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = VitalSigns # form_class = VitalSignsForm # template_name = 'emr/vital_signs_form.html' # success_message = _('Vital signs recorded successfully.') # # def form_valid(self, form): # form.instance.recorded_by = self.request.user # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='VitalSigns', # object_id=str(self.object.pk), # changes={'status': 'Vital signs recorded'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:vital_signs_detail', kwargs={'pk': self.object.pk}) # # # # ProblemList --------------------------------------------------------------------- # # class ProblemListListView(LoginRequiredMixin, TenantMixin, ListView): # model = ProblemList # template_name = 'emr/problem_list.html' # context_object_name = 'problems' # paginate_by = 20 # # def get_queryset(self): # return super().get_queryset().select_related('patient','identified_by').order_by('-created_at') # # # class ProblemListDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = ProblemList # template_name = 'emr/problem_detail.html' # context_object_name = 'problem' # # # class ProblemListCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = ProblemList # form_class = ProblemListForm # template_name = 'emr/problem_form.html' # success_message = _('Problem added successfully.') # # def form_valid(self, form): # form.instance.identified_by = self.request.user # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='ProblemList', # object_id=str(self.object.pk), # changes={'status': 'Problem added'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:problem_detail', kwargs={'pk': self.object.pk}) # # # class ProblemListUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = ProblemList # form_class = ProblemListForm # template_name = 'emr/problem_form.html' # success_message = _('Problem updated successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='ProblemList', # object_id=str(self.object.pk), # changes={'status': 'Problem updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:problem_detail', kwargs={'pk': self.object.pk}) # # # class ProblemListDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = ProblemList # template_name = 'emr/problem_confirm_delete.html' # success_url = reverse_lazy('emr:problem_list') # success_message = _('Problem deleted successfully.') # # def delete(self, request, *args, **kwargs): # prob = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='ProblemList', # object_id=str(prob.pk), # changes={'status': 'Problem deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # CarePlan ------------------------------------------------------------------------ # # class CarePlanListView(LoginRequiredMixin, TenantMixin, ListView): # model = CarePlan # template_name = 'emr/care_plan_list.html' # context_object_name = 'care_plans' # paginate_by = 20 # # # class CarePlanDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = CarePlan # template_name = 'emr/care_plan_detail.html' # context_object_name = 'care_plan' # # # class CarePlanCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = CarePlan # form_class = CarePlanForm # template_name = 'emr/care_plan_form.html' # success_message = _('Care plan created successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='CarePlan', # object_id=str(self.object.pk), # changes={'status': 'Care plan created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:care_plan_detail', kwargs={'pk': self.object.pk}) # # # class CarePlanUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = CarePlan # form_class = CarePlanForm # template_name = 'emr/care_plan_form.html' # success_message = _('Care plan updated successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='CarePlan', # object_id=str(self.object.pk), # changes={'status': 'Care plan updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:care_plan_detail', kwargs={'pk': self.object.pk}) # # # class CarePlanDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = CarePlan # template_name = 'emr/care_plan_confirm_delete.html' # success_url = reverse_lazy('emr:care_plan_list') # success_message = _('Care plan deleted successfully.') # # def delete(self, request, *args, **kwargs): # cp = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='CarePlan', # object_id=str(cp.pk), # changes={'status': 'Care plan deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # ClinicalNote -------------------------------------------------------------------- # # class ClinicalNoteListView(LoginRequiredMixin, TenantMixin, ListView): # model = ClinicalNote # template_name = 'emr/clinical_note_list.html' # context_object_name = 'notes' # paginate_by = 20 # # # class ClinicalNoteDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = ClinicalNote # template_name = 'emr/clinical_note_detail.html' # context_object_name = 'note' # # # class ClinicalNoteCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = ClinicalNote # form_class = ClinicalNoteForm # template_name = 'emr/clinical_note_form.html' # success_message = _('Clinical note created successfully.') # # def form_valid(self, form): # form.instance.author = self.request.user # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='ClinicalNote', # object_id=str(self.object.pk), # changes={'status': 'Clinical note created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) # # # class ClinicalNoteUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = ClinicalNote # form_class = ClinicalNoteForm # template_name = 'emr/clinical_note_form.html' # success_message = _('Clinical note updated successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='ClinicalNote', # object_id=str(self.object.pk), # changes={'status': 'Clinical note updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) # # # class ClinicalNoteDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = ClinicalNote # template_name = 'emr/clinical_note_confirm_delete.html' # success_url = reverse_lazy('emr:clinical_note_list') # success_message = _('Clinical note deleted successfully.') # # def delete(self, request, *args, **kwargs): # cn = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='ClinicalNote', # object_id=str(cn.pk), # changes={'status': 'Clinical note deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # NoteTemplate ------------------------------------------------------------------- # # class NoteTemplateListView(LoginRequiredMixin, TenantMixin, ListView): # model = NoteTemplate # template_name = 'emr/note_template_list.html' # context_object_name = 'templates' # paginate_by = 20 # # # class NoteTemplateDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = NoteTemplate # template_name = 'emr/note_template_detail.html' # context_object_name = 'template' # # # class NoteTemplateCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = NoteTemplate # form_class = NoteTemplateForm # template_name = 'emr/note_template_form.html' # success_message = _('Note template created successfully.') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='NoteTemplate', # object_id=str(self.object.pk), # changes={'status': 'Template created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:note_template_detail', kwargs={'pk': self.object.pk}) # # # class NoteTemplateUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = NoteTemplate # form_class = NoteTemplateForm # template_name = 'emr/note_template_form.html' # success_message = _('Note template updated successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='NoteTemplate', # object_id=str(self.object.pk), # changes={'status': 'Template updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:note_template_detail', kwargs={'pk': self.object.pk}) # # # class NoteTemplateDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = NoteTemplate # template_name = 'emr/note_template_confirm_delete.html' # success_url = reverse_lazy('emr:note_template_list') # success_message = _('Note template deleted successfully.') # # def delete(self, request, *args, **kwargs): # nt = self.get_object() # if ClinicalNote.objects.filter(template=nt).exists(): # messages.error(request, _('Cannot delete a template in use.')) # return redirect('emr:note_template_detail', pk=nt.pk) # # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='NoteTemplate', # object_id=str(nt.pk), # changes={'status': 'Template deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # HTMX & Actions ------------------------------------------------------------------ # # @login_required # def htmx_emr_stats(request): # tenant = request.user.tenant # today = timezone.now().date() # enc = Encounter.objects.filter(patient__tenant=tenant) # vs = VitalSigns.objects.filter(encounter__patient__tenant=tenant) # pb = ProblemList.objects.filter(patient__tenant=tenant) # cp = CarePlan.objects.filter(patient__tenant=tenant) # cn = ClinicalNote.objects.filter(encounter__patient__tenant=tenant) # # stats = { # 'total_encounters': enc.count(), # 'encounters_today': enc.filter(scheduled_datetime__date=today).count(), # 'active_encounters': enc.filter(status='IN_PROGRESS').count(), # 'total_vital_signs': vs.count(), # 'vital_signs_today': vs.filter(recorded_at__date=today).count(), # 'active_problems': pb.filter(status='ACTIVE').count(), # 'active_care_plans': cp.filter(status='ACTIVE').count(), # 'total_notes': cn.count(), # 'notes_today': cn.filter(created_at__date=today).count(), # } # return JsonResponse(stats) # # # @login_required # def start_encounter(request, pk): # enc = get_object_or_404(Encounter, pk=pk, patient__tenant=request.user.tenant) # if enc.status == 'SCHEDULED': # enc.status = 'IN_PROGRESS'; enc.save() # AuditLogEntry.objects.create( # tenant=request.user.tenant, user=request.user, # action='UPDATE', model_name='Encounter', # object_id=str(enc.pk), changes={'status': 'Encounter started'} # ) # messages.success(request, _('Encounter started.')) # else: # messages.error(request, _('Only scheduled encounters can be started.')) # return redirect('emr:encounter_detail', pk=pk) # # # @login_required # def complete_encounter(request, pk): # enc = get_object_or_404(Encounter, pk=pk, patient__tenant=request.user.tenant) # if enc.status == 'IN_PROGRESS': # enc.status = 'COMPLETED'; enc.save() # AuditLogEntry.objects.create( # tenant=request.user.tenant, user=request.user, # action='UPDATE', model_name='Encounter', # object_id=str(enc.pk), changes={'status': 'Encounter completed'} # ) # messages.success(request, _('Encounter completed.')) # else: # messages.error(request, _('Only in-progress encounters can be completed.')) # return redirect('emr:encounter_detail', pk=pk) # # @login_required def resolve_problem(request, pk): prob = get_object_or_404(ProblemList, pk=pk, patient__tenant=request.user.tenant) if prob.status == 'ACTIVE': prob.status = 'RESOLVED'; prob.save() AuditLogEntry.objects.create( tenant=request.user.tenant, user=request.user, action='UPDATE', model_name='ProblemList', object_id=str(prob.pk), changes={'status': 'Problem resolved'} ) messages.success(request, _('Problem resolved.')) else: messages.error(request, _('Only active problems can be resolved.')) return redirect('emr:problem_detail', pk=pk) # # # @login_required # def complete_care_plan(request, pk): # cp = get_object_or_404(CarePlan, pk=pk, patient__tenant=request.user.tenant) # if cp.status == 'ACTIVE': # cp.status = 'COMPLETED'; cp.save() # AuditLogEntry.objects.create( # tenant=request.user.tenant, user=request.user, # action='UPDATE', model_name='CarePlan', # object_id=str(cp.pk), changes={'status': 'Care plan completed'} # ) # messages.success(request, _('Care plan completed.')) # else: # messages.error(request, _('Only active care plans can be completed.')) # return redirect('emr:care_plan_detail', pk=pk) # # # @login_required # def sign_note(request, pk): # note = get_object_or_404(ClinicalNote, pk=pk, encounter__patient__tenant=request.user.tenant) # if note.status == 'DRAFT' and note.author == request.user: # note.status = 'SIGNED'; note.signed_at = timezone.now(); note.save() # AuditLogEntry.objects.create( # tenant=request.user.tenant, user=request.user, # action='UPDATE', model_name='ClinicalNote', # object_id=str(note.pk), changes={'status': 'Clinical note signed'} # ) # messages.success(request, _('Note signed.')) # else: # messages.error(request, _('Only your draft notes can be signed.')) # return redirect('emr:clinical_note_detail', pk=pk) # # # class PatientEMRView(LoginRequiredMixin, DetailView): # model = PatientProfile # template_name = 'emr/patient_emr.html' # pk_url_kwarg = 'patient_id' # # def get_queryset(self): # return PatientProfile.objects.filter(tenant=self.request.user.tenant) # # # @login_required # def htmx_encounter_search(request): # search = request.GET.get('search', '') # encs = Encounter.objects.filter(patient__tenant=request.user.tenant) # if search: # encs = encs.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(chief_complaint__icontains=search) # ) # encs = encs.order_by('-scheduled_datetime')[:10] # return render(request, 'emr/partials/encounter_list.html', {'encounters': encs}) # # # @login_required # def htmx_vital_signs_search(request): # search = request.GET.get('search', '') # vs = VitalSigns.objects.filter(encounter__patient__tenant=request.user.tenant) # if search: # vs = vs.filter( # Q(encounter__patient__first_name__icontains=search) | # Q(encounter__patient__last_name__icontains=search) # ) # vs = vs.order_by('-recorded_at')[:10] # return render(request, 'emr/partials/vital_signs_list.html', {'vital_signs': vs}) # # # class VitalSignsUpdateView(LoginRequiredMixin, FormKwargsMixin, SuccessMessageMixin, TenantMixin, UpdateView): # model = VitalSigns # form_class = VitalSignsForm # template_name = 'emr/vital_signs_form.html' # success_message = _('Vital signs updated successfully.') # # implement form_valid, get_success_url, etc. # # class VitalSignsDeleteView(LoginRequiredMixin, TenantMixin, SuccessMessageMixin, DeleteView): # model = VitalSigns # template_name = 'emr/vital_signs_confirm_delete.html' # success_url = reverse_lazy('emr:vitalsigns_list') # success_message = _('Vital signs entry deleted.') # # @login_required # def get_template_content(request, pk): # """ # Return the raw content of a NoteTemplate (e.g. for AJAX/HTMX). # """ # template = get_object_or_404( # NoteTemplate, # pk=pk, # tenant=request.user.tenant # ) # return JsonResponse({ # 'template_id': str(template.template_id), # 'name': template.name, # 'content': template.content, # }) #