Marwan Alwali 4d06ca4b5e update
2025-09-20 14:26:19 +03:00

1882 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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, reverse
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
from django.http import JsonResponse, HttpResponse, Http404
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
from django.utils.dateformat import format as dj_format
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import *
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/encounters/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/encounters/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/encounters/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/problems/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_plans/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_notes/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_notes/clinical_note_detail.html'
context_object_name = 'clinical_note'
class ClinicalNoteCreateView(LoginRequiredMixin, CreateView):
model = ClinicalNote
form_class = ClinicalNoteForm
template_name = 'emr/clinical_notes/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,UpdateView):
model = ClinicalNote
form_class = ClinicalNoteForm
template_name = 'emr/clinical_notes/clinical_note_form.html'
success_message = _('Clinical note updated successfully.')
def dispatch(self, request, *args, **kwargs):
# Ensure tenant exists
self.tenant = request.user.tenant
if not self.tenant:
return JsonResponse({"error": "No tenant found"}, status=400)
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='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})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
encounter = get_object_or_404(Encounter, pk=self.kwargs['pk'], tenant=self.tenant)
ctx['patient'] = encounter.patient
ctx['encounter'] = encounter
ctx['author'] = self.request.user # or Employee profile if you have one
return ctx
class ClinicalNoteDeleteView(LoginRequiredMixin,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)
#
# # 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, DetailView):
model = VitalSigns
template_name = 'emr/vital_signs/vital_signs_detail.html'
context_object_name = 'vital_signs'
class VitalSignsCreateView(LoginRequiredMixin, CreateView):
model = VitalSigns
form_class = VitalSignsForm
template_name = 'emr/vital_signs/vital_signs_form.html'
success_message = _('Vital signs recorded successfully.')
def dispatch(self, request, *args, **kwargs):
# Ensure tenant exists
self.tenant = getattr(request, "tenant", None)
if not self.tenant:
return JsonResponse({"error": "No tenant found"}, status=400)
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# pass what the form actually expects
kwargs['tenant'] = self.tenant
kwargs['user'] = self.request.user
# (Optional) initial values for fields that ARE in the form:
# kwargs.setdefault('initial', {})
# kwargs['initial'].setdefault('measured_datetime', timezone.now())
return kwargs
def form_valid(self, form):
# set server-controlled fields not present on the form
form.instance.measured_by = self.request.user
encounter = get_object_or_404(
Encounter,
pk=self.kwargs['pk'],
tenant=self.tenant
)
form.instance.encounter = encounter
form.instance.patient = encounter.patient
response = super().form_valid(form)
AuditLogEntry.objects.create(
tenant=self.tenant, # use the resolved 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})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
# encounter is coming from URL; fetch it with tenant check
encounter = get_object_or_404(
Encounter, pk=self.kwargs['pk'], tenant=self.tenant
)
ctx['patient'] = encounter.patient
ctx['encounter'] = encounter
ctx['measured_by'] = self.request.user # or Employee profile if you have one
return ctx
class ProblemListListView(LoginRequiredMixin, ListView):
model = ProblemList
template_name = 'emr/problems/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/problems/problem_detail.html'
context_object_name = 'problem'
class ProblemListCreateView(LoginRequiredMixin, CreateView):
model = ProblemList
form_class = ProblemListForm
template_name = 'emr/problems/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, UpdateView):
model = ProblemList
form_class = ProblemListForm
template_name = 'emr/problems/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, DeleteView):
model = ProblemList
template_name = 'emr/problems/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)
#
class CarePlanDetailView(LoginRequiredMixin, DetailView):
model = CarePlan
template_name = 'emr/care_plans/care_plan_detail.html'
context_object_name = 'care_plan'
class CarePlanCreateView(LoginRequiredMixin, CreateView):
model = CarePlan
form_class = CarePlanForm
template_name = 'emr/care_plans/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, UpdateView):
model = CarePlan
form_class = CarePlanForm
template_name = 'emr/care_plans/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 CarePlanProgressUpdateView(LoginRequiredMixin, UpdateView):
"""
Update only progress-related fields of a CarePlan.
"""
model = CarePlan
form_class = CarePlanProgressForm
template_name = 'emr/care_plans/care_plan_progress_form.html'
# permission_required = 'emr.change_careplan'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request, 'tenant', None)
return kwargs
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
if not tenant:
return CarePlan.objects.none()
# Limit by tenant, and optionally eager-load relations used in your template
return CarePlan.objects.filter(tenant=tenant)
def form_valid(self, form):
# Auto-set last_reviewed to today if user left it blank
if not form.cleaned_data.get('last_reviewed'):
form.instance.last_reviewed = timezone.now().date()
# If user marks plan completed and end_date is empty, set end_date to today
if form.cleaned_data.get('status') == 'COMPLETED' and not form.instance.end_date:
form.instance.end_date = timezone.now().date()
response = super().form_valid(form)
try:
AuditLogger.log_event(
tenant=form.instance.tenant,
event_type='UPDATE',
event_category='CARE_PLAN',
action='Update Care Plan Progress',
description=f'Progress updated for care plan #{self.object.pk} ({self.object.title})',
user=self.request.user,
content_object=self.object,
request=self.request,
)
except Exception:
# Dont block user flow if logging fails
pass
messages.success(self.request, 'Care plan progress updated successfully.')
return response
def get_success_url(self):
return reverse('emr:care_plan_detail', args=[self.object.pk])
class CarePlanDeleteView(LoginRequiredMixin, DeleteView):
model = CarePlan
template_name = 'emr/care_plans/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()
AuditLogger.log_event(
tenant=request.user.tenant,
event_type='UPDATE',
event_category='CARE_PLAN',
action='Care plan completed',
description=f'Progress updated for care plan #{cp.pk} ({cp.title})',
user=request.user,
content_object=cp,
request=request,
)
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 approve_care_plan(request, pk):
cp = get_object_or_404(CarePlan, pk=pk, patient__tenant=request.user.tenant)
if cp.status == 'DRAFT':
cp.status = 'ACTIVE'
cp.save()
AuditLogger.log_event(
tenant=request.user.tenant,
event_type='UPDATE',
event_category='CARE_PLAN',
action='Care plan approved',
description=f'Approval of care plan #{cp.pk} ({cp.title})',
user=request.user,
content_object=cp,
request=request,
)
messages.success(request, _('Care plan approved.'))
else:
messages.error(request, _('Only pending care plans can be approved.'))
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 vital_signs_search(request):
tenant = request.user.tenant
search = request.GET.get('search', '')
vs = VitalSigns.objects.filter(encounter__patient__tenant=tenant)
if search:
vs = vs.filter(
Q(encounter__patient__first_name__icontains=search) |
Q(encounter__patient__last_name__icontains=search)
)
vs = vs.order_by('-measured_datetime')[:10]
return render(request, 'emr/partials/vital_signs_search.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.template_content,
})
@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.
"""
tenant = request.user.tenant
patient = get_object_or_404(
PatientProfile,
id=patient_id,
tenant=tenant
)
# Get recent vital signs
vital_signs = VitalSigns.objects.filter(
patient=patient
).order_by('-measured_datetime')[:15]
context = {
'patient': patient,
'vital_signs': vital_signs
}
return render(request, 'emr/partials/vital_signs_chart.html', context)
@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, pk):
"""
HTMX endpoint for adding vital signs.
"""
tenant = request.user.tenant
if request.method == 'POST':
encounter = get_object_or_404(
Encounter,
pk=pk,
tenant=tenant
)
vital_signs_data = {
'encounter': encounter,
'patient': encounter.patient,
'measured_by': request.user,
'measured_datetime': timezone.now(),
}
# Extract vital signs data
# 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')
else:
messages.error(request, 'An error occurred while recording the vital signs. Please try again later.')
# return JsonResponse({'success': True, 'vital_signs_id': vital_signs.id})
return redirect('emr:encounter_detail', pk=pk)
# 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.
"""
tenant = request.user.tenant
if request.method == 'POST':
note = get_object_or_404(
ClinicalNote,
id=note_id,
patient__tenant=tenant,
author=request.user,
status='DRAFT'
)
note.status = 'SIGNED'
note.electronically_signed = True
note.signed_datetime = timezone.now()
note.signature_method = 'ELECTRONIC'
note.save()
# Log the action
AuditLogger.log_event(
tenant=tenant,
event_type='NOTE_SIGNED',
event_category='ClinicalNote',
action='Sign clinical note',
description=f"Clinical note signed: {note.title}",
user=request.user,
content_object=note,
request=request
)
messages.success(request, f'Note "{note.title}" signed successfully')
else:
messages.error(request, 'An error occurred while signing the note. Please try again later.')
return redirect('emr:clinical_note_detail', id=note_id)
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')
def _norm_code(s: str) -> str:
return (s or "").upper().replace(" ", "")
class Icd10SearchView(LoginRequiredMixin, ListView):
"""
Search ICD-10 by description or code.
URL params: ?q=...&mode=description|code&page=N
"""
model = Icd10
template_name = "emr/icd10_search.html"
context_object_name = "results"
paginate_by = 20
def get_queryset(self):
q = (self.request.GET.get("q") or "").strip()
mode = (self.request.GET.get("mode") or "description").strip().lower()
if not q:
return Icd10.objects.none()
try:
if mode == "code":
qn = _norm_code(q)
return (
Icd10.objects.filter(
Q(code__iexact=qn)
| Q(code__istartswith=qn)
| Q(code__icontains=qn)
)
.annotate(
exact_first=Case(
When(code__iexact=qn, then=Value(0)),
When(code__istartswith=qn, then=Value(1)),
default=Value(2),
output_field=IntegerField(),
)
)
.order_by("exact_first", "code")
)
else:
return (
Icd10.objects.filter(
Q(description__icontains=q)
| Q(section_name__icontains=q)
| Q(chapter_name__icontains=q)
)
.order_by("code")
)
except Exception as exc:
messages.error(self.request, f"Search failed: {exc}")
return Icd10.objects.none()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["q"] = self.request.GET.get("q", "")
ctx["mode"] = self.request.GET.get("mode", "description")
ctx["total"] = self.get_queryset().count()
ctx["error"] = None # template compatibility
return ctx
class Icd10DetailView(LoginRequiredMixin, DetailView):
"""
Exact lookup by code (e.g., A00.0).
"""
model = Icd10
template_name = "emr/icd10_detail.html"
context_object_name = "record"
slug_field = "code"
slug_url_kwarg = "code"
def get_object(self, queryset=None):
code_in = (self.kwargs.get(self.slug_url_kwarg) or "").strip()
if not code_in:
raise Http404("ICD-10 code is required.")
try:
return self.model.objects.get(code__iexact=_norm_code(code_in))
except self.model.DoesNotExist:
raise Http404("ICD-10 code not found.")
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
record = ctx["record"]
# Collect ancestors
ancestors = []
p = record.parent
while p is not None:
ancestors.append(p)
p = p.parent
ancestors.reverse()
ctx["code"] = record.code
ctx["ancestors"] = ancestors
ctx["children"] = record.children.all().order_by("code")
return ctx
# from .utils import search_by_description, search_by_code, get_by_code
# from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
#
# def icd10_search_view(request):
# """
# Search ICD-10 by description or code.
# ?q=...&mode=description|code
# """
# q = (request.GET.get('q') or "").strip()
# mode = (request.GET.get('mode') or "description").strip().lower()
#
# results = []
# total = 0
# page_obj = None
# error = None
#
# if q:
# try:
# if mode == "code":
# results = list(search_by_code(q) or [])
# else:
# # default to description search
# results = list(search_by_description(q) or [])
# total = len(results)
# except Exception as exc:
# error = str(exc)
# messages.error(request, f"Search failed: {error}")
# results = []
# total = 0
#
# # paginate lists (skip if it looks like a single dict/object)
# if isinstance(results, (list, tuple)):
# paginator = Paginator(results, 20) # 20 per page
# page = request.GET.get('page')
# try:
# page_obj = paginator.get_page(page)
# except (PageNotAnInteger, EmptyPage):
# page_obj = paginator.get_page(1)
# else:
# page_obj = None # single record / non-iterable
#
# context = {
# "query": q,
# "mode": mode,
# "results": results,
# "page_obj": page_obj,
# "total": total,
# "error": error,
# }
# return render(request, "emr/icd10_search.html", context)
#
# def icd10_detail_view(request, code):
# """
# Exact lookup by code (e.g., A00.0).
# """
# code = (code or "").strip()
# if not code:
# raise Http404("ICD-10 code is required.")
#
# try:
# record = get_by_code(code)
# if not record:
# raise Http404("ICD-10 code not found.")
# except Exception as exc:
# messages.error(request, f"Lookup failed: {exc}")
# raise Http404("ICD-10 code not found.")
#
# # 'record' could be a dict-like or object; normalize a bit for template
# # We won't assume exact keys; template will render keys/values safely.
# context = {"record": record, "code": code}
# return render(request, "emr/icd10_detail.html", context)