4714 lines
173 KiB
Python
4714 lines
173 KiB
Python
"""
|
||
Patients app views for hospital management system with comprehensive CRUD operations.
|
||
"""
|
||
import uuid
|
||
|
||
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, PermissionRequiredMixin
|
||
from django.views.generic import (
|
||
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
|
||
)
|
||
from django.http import JsonResponse
|
||
from django.contrib import messages
|
||
from django.db.models import Q, Count
|
||
from django.utils import timezone
|
||
from django.urls import reverse_lazy, reverse
|
||
from django.core.paginator import Paginator
|
||
from datetime import timedelta, date
|
||
|
||
from appointments.models import AppointmentRequest
|
||
from core.models import AuditLogEntry
|
||
from .models import (
|
||
PatientProfile, EmergencyContact, InsuranceInfo,
|
||
ConsentTemplate, ConsentForm, PatientNote
|
||
)
|
||
from .forms import (
|
||
PatientProfileForm, EmergencyContactForm, InsuranceInfoForm,
|
||
ConsentTemplateForm, ConsentFormForm, PatientNoteForm, PatientsSearchForm
|
||
)
|
||
from core.utils import AuditLogger
|
||
|
||
|
||
class PatientListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
Patient listing view.
|
||
"""
|
||
model = PatientProfile
|
||
template_name = 'patients/profiles/patient_list.html'
|
||
context_object_name = 'patients'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return PatientProfile.objects.none()
|
||
|
||
queryset = PatientProfile.objects.filter(tenant=tenant)
|
||
|
||
# Apply filters
|
||
gender = self.request.GET.get('gender')
|
||
if gender:
|
||
queryset = queryset.filter(gender=gender)
|
||
|
||
age_range = self.request.GET.get('age_range')
|
||
if age_range:
|
||
today = timezone.now().date()
|
||
if age_range == 'pediatric':
|
||
# Under 18
|
||
min_birth_date = today.replace(year=today.year - 18)
|
||
queryset = queryset.filter(date_of_birth__gt=min_birth_date)
|
||
elif age_range == 'adult':
|
||
# 18-64
|
||
min_birth_date = today.replace(year=today.year - 65)
|
||
max_birth_date = today.replace(year=today.year - 18)
|
||
queryset = queryset.filter(
|
||
date_of_birth__gt=min_birth_date,
|
||
date_of_birth__lte=max_birth_date
|
||
)
|
||
elif age_range == 'geriatric':
|
||
# 65+
|
||
max_birth_date = today.replace(year=today.year - 65)
|
||
queryset = queryset.filter(date_of_birth__lte=max_birth_date)
|
||
|
||
blood_type = self.request.GET.get('blood_type')
|
||
if blood_type:
|
||
queryset = queryset.filter(blood_type=blood_type)
|
||
|
||
status = self.request.GET.get('status')
|
||
if status == 'active':
|
||
queryset = queryset.filter(is_active=True)
|
||
elif status == 'inactive':
|
||
queryset = queryset.filter(is_active=False)
|
||
|
||
search = self.request.GET.get('search')
|
||
if search:
|
||
queryset = queryset.filter(
|
||
Q(patient_id__icontains=search) |
|
||
Q(first_name__icontains=search) |
|
||
Q(last_name__icontains=search) |
|
||
Q(email__icontains=search) |
|
||
Q(phone_number__icontains=search) |
|
||
Q(mobile_number__icontains=search)
|
||
)
|
||
|
||
return queryset.order_by('last_name', 'first_name')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
|
||
if tenant:
|
||
context.update({
|
||
'search_form': PatientsSearchForm(self.request.GET),
|
||
'total_patients': PatientProfile.objects.filter(tenant=tenant).count(),
|
||
'active_patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).count(),
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
class PatientDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Patient detail view.
|
||
"""
|
||
model = PatientProfile
|
||
template_name = 'patients/profiles/patient_detail.html'
|
||
context_object_name = 'patient'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return PatientProfile.objects.none()
|
||
return PatientProfile.objects.filter(tenant=tenant)
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
patient = self.get_object()
|
||
|
||
# Get related data
|
||
context['emergency_contacts'] = EmergencyContact.objects.filter(
|
||
patient=patient
|
||
).order_by('-is_primary')
|
||
|
||
context['insurance_info'] = InsuranceInfo.objects.filter(
|
||
patient=patient
|
||
).order_by('-is_active', '-effective_date')
|
||
|
||
context['consent_forms'] = ConsentForm.objects.filter(
|
||
patient=patient
|
||
).order_by('-created_at')
|
||
|
||
context['patient_notes'] = PatientNote.objects.filter(
|
||
patient=patient
|
||
).order_by('-created_at')[:10]
|
||
|
||
# Calculate age
|
||
if patient.date_of_birth:
|
||
today = date.today()
|
||
age = today.year - patient.date_of_birth.year
|
||
if today.month < patient.date_of_birth.month or \
|
||
(today.month == patient.date_of_birth.month and today.day < patient.date_of_birth.day):
|
||
age -= 1
|
||
context['patient_age'] = age
|
||
|
||
return context
|
||
|
||
|
||
class PatientCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
"""
|
||
Create new patient.
|
||
"""
|
||
model = PatientProfile
|
||
form_class = PatientProfileForm
|
||
template_name = 'patients/patient_registration.html'
|
||
permission_required = 'patients.add_patientprofile'
|
||
success_url = reverse_lazy('patients:patient_list')
|
||
|
||
def form_valid(self, form):
|
||
# Set tenant
|
||
form.instance.tenant = getattr(self.request, 'tenant', None)
|
||
response = super().form_valid(form)
|
||
|
||
# Log patient creation
|
||
AuditLogger.log_event(
|
||
tenant=form.instance.tenant,
|
||
event_type='CREATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Create Patient',
|
||
description=f'Created new patient: {self.object.first_name} {self.object.last_name}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Patient "{self.object.first_name} {self.object.last_name}" created successfully.')
|
||
return response
|
||
|
||
|
||
class PatientUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
"""
|
||
Update patient information.
|
||
"""
|
||
model = PatientProfile
|
||
form_class = PatientProfileForm
|
||
template_name = 'patients/profiles/patient_form.html'
|
||
permission_required = 'patients.change_patientprofile'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return PatientProfile.objects.none()
|
||
return PatientProfile.objects.filter(tenant=tenant)
|
||
|
||
def get_success_url(self):
|
||
return reverse('patients:patient_detail', kwargs={'pk': self.object.pk})
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
|
||
# Log patient update
|
||
AuditLogger.log_event(
|
||
tenant=self.object.tenant,
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Update Patient',
|
||
description=f'Updated patient: {self.object.first_name} {self.object.last_name}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Patient "{self.object.first_name} {self.object.last_name}" updated successfully.')
|
||
return response
|
||
|
||
|
||
class PatientDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
"""
|
||
Delete patient (soft delete to inactive).
|
||
"""
|
||
model = PatientProfile
|
||
template_name = 'patients/profiles/patient_confirm_delete.html'
|
||
permission_required = 'patients.delete_patientprofile'
|
||
success_url = reverse_lazy('patients:patient_list')
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return PatientProfile.objects.none()
|
||
return PatientProfile.objects.filter(tenant=tenant)
|
||
|
||
def delete(self, request, *args, **kwargs):
|
||
self.object = self.get_object()
|
||
|
||
# Soft delete - set to inactive
|
||
self.object.is_active = False
|
||
self.object.save()
|
||
|
||
# Log patient deletion
|
||
AuditLogger.log_event(
|
||
tenant=self.object.tenant,
|
||
event_type='DELETE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Deactivate Patient',
|
||
description=f'Deactivated patient: {self.object.first_name} {self.object.last_name}',
|
||
user=request.user,
|
||
content_object=self.object,
|
||
request=request
|
||
)
|
||
|
||
messages.success(request, f'Patient "{self.object.first_name} {self.object.last_name}" deactivated successfully.')
|
||
return redirect(self.success_url)
|
||
|
||
|
||
class EmergencyContactListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
List emergency contacts.
|
||
"""
|
||
model = EmergencyContact
|
||
template_name = 'patients/emergency_contacts/emergency_contact_list.html'
|
||
context_object_name = 'emergency_contacts'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return EmergencyContact.objects.none()
|
||
|
||
queryset = EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
|
||
# Apply filters
|
||
patient_id = self.request.GET.get('patient_id')
|
||
if patient_id:
|
||
queryset = queryset.filter(patient_id=patient_id)
|
||
|
||
relationship = self.request.GET.get('relationship')
|
||
if relationship:
|
||
queryset = queryset.filter(relationship__icontains=relationship)
|
||
|
||
is_primary = self.request.GET.get('is_primary')
|
||
if is_primary == 'true':
|
||
queryset = queryset.filter(is_primary=True)
|
||
elif is_primary == 'false':
|
||
queryset = queryset.filter(is_primary=False)
|
||
|
||
return queryset.order_by('patient__last_name', '-is_primary', 'first_name')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
|
||
if tenant:
|
||
context.update({
|
||
'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'),
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
class EmergencyContactDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Display emergency contact details.
|
||
"""
|
||
model = EmergencyContact
|
||
template_name = 'patients/emergency_contacts/emergency_contact_detail.html'
|
||
context_object_name = 'emergency_contact'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return EmergencyContact.objects.none()
|
||
return EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
|
||
|
||
class EmergencyContactCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
"""
|
||
Create a new emergency contact for a specific patient (passed as <int:pk> in the URL).
|
||
"""
|
||
model = EmergencyContact
|
||
form_class = EmergencyContactForm
|
||
template_name = "patients/emergency_contacts/emergency_contact_form.html"
|
||
permission_required = "patients.add_emergencycontact"
|
||
# if you always go back to the list, keep this; otherwise override get_success_url below
|
||
success_url = reverse_lazy("patients:emergency_contact_list")
|
||
|
||
def dispatch(self, request, *args, **kwargs):
|
||
# Ensure tenant exists (if you follow multi-tenant pattern)
|
||
self.tenant = getattr(request, "tenant", None)
|
||
if not self.tenant:
|
||
return JsonResponse({"error": "No tenant found"}, status=400)
|
||
|
||
# Fetch the patient from URL and ensure it belongs to the tenant
|
||
self.patient = get_object_or_404(
|
||
PatientProfile,
|
||
pk=self.kwargs.get("pk"),
|
||
tenant=self.tenant,
|
||
is_active=True,
|
||
)
|
||
return super().dispatch(request, *args, **kwargs)
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
# pass user for your form's tenant scoping
|
||
kwargs["user"] = self.request.user
|
||
# prefill the patient
|
||
kwargs.setdefault("initial", {})
|
||
kwargs["initial"]["patient"] = self.patient
|
||
return kwargs
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
ctx["patient"] = self.patient
|
||
return ctx
|
||
|
||
def form_valid(self, form):
|
||
# Enforce the patient from the URL regardless of any posted value
|
||
form.instance.patient = self.patient
|
||
response = super().form_valid(form)
|
||
|
||
# Log emergency contact creation (fix name fields)
|
||
contact_name = f"{self.object.first_name} {self.object.last_name}".strip()
|
||
AuditLogger.log_event(
|
||
tenant=self.tenant,
|
||
event_type="CREATE",
|
||
event_category="PATIENT_MANAGEMENT",
|
||
action="Create Emergency Contact",
|
||
description=f'Created emergency contact: {contact_name} for patient {self.patient}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request,
|
||
)
|
||
|
||
messages.success(self.request, f'Emergency contact "{contact_name}" created successfully.')
|
||
return response
|
||
|
||
# Optional: redirect back to the patient’s detail page (or their contact list)
|
||
def get_success_url(self):
|
||
return reverse("patients:patient_detail", args=[self.patient.pk])
|
||
|
||
|
||
class EmergencyContactUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
"""
|
||
Update emergency contact.
|
||
"""
|
||
model = EmergencyContact
|
||
form_class = EmergencyContactForm
|
||
template_name = 'patients/emergency_contacts/emergency_contact_form.html'
|
||
permission_required = 'patients.change_emergencycontact'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return EmergencyContact.objects.none()
|
||
return EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
kwargs['user'] = self.request.user
|
||
return kwargs
|
||
|
||
def get_success_url(self):
|
||
return reverse('patients:emergency_contact_detail', kwargs={'pk': self.object.pk})
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
|
||
# Log emergency contact update
|
||
AuditLogger.log_event(
|
||
tenant=getattr(self.request, 'tenant', None),
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Update Emergency Contact',
|
||
description=f'Updated emergency contact: {self.object.name}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Emergency contact "{self.object.name}" updated successfully.')
|
||
return response
|
||
|
||
|
||
class EmergencyContactDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
"""
|
||
Delete emergency contact.
|
||
"""
|
||
model = EmergencyContact
|
||
template_name = 'patients/emergency_contacts/emergency_contact_confirm_delete.html'
|
||
permission_required = 'patients.delete_emergencycontact'
|
||
success_url = reverse_lazy('patients:emergency_contact_list')
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return EmergencyContact.objects.none()
|
||
return EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
|
||
def delete(self, request, *args, **kwargs):
|
||
self.object = self.get_object()
|
||
contact_name = self.object.name
|
||
|
||
# Log emergency contact deletion
|
||
AuditLogger.log_event(
|
||
tenant=getattr(request, 'tenant', None),
|
||
event_type='DELETE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Delete Emergency Contact',
|
||
description=f'Deleted emergency contact: {contact_name}',
|
||
user=request.user,
|
||
content_object=self.object,
|
||
request=request
|
||
)
|
||
|
||
messages.success(request, f'Emergency contact "{contact_name}" deleted successfully.')
|
||
return super().delete(request, *args, **kwargs)
|
||
|
||
|
||
class InsuranceInfoListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
List insurance information, optionally scoped to a specific patient via URL <int:pk>.
|
||
Supports filters:
|
||
- patient_id
|
||
- insurance_company (icontains)
|
||
- is_primary: true|false
|
||
- expiry_status: expired|expiring_soon
|
||
- search: matches patient name/company/policy number (optional example)
|
||
- per_page: pagination size
|
||
"""
|
||
model = InsuranceInfo
|
||
template_name = "patients/insurance/insurance_list.html"
|
||
context_object_name = "insurance_info"
|
||
paginate_by = 25 # default; can be overridden by ?per_page=
|
||
|
||
# If you define a patient-specific URL like:
|
||
# path('patients/<int:pk>/insurance/', InsuranceInfoListView.as_view(), ...)
|
||
# this view will scope to that patient.
|
||
def dispatch(self, request, *args, **kwargs):
|
||
self.tenant = getattr(request, "tenant", None)
|
||
if not self.tenant:
|
||
return JsonResponse({"error": "No tenant found"}, status=400)
|
||
|
||
self.patient = None
|
||
if "pk" in self.kwargs and self.kwargs["pk"] is not None:
|
||
self.patient = get_object_or_404(
|
||
PatientProfile,
|
||
pk=self.kwargs["pk"],
|
||
tenant=self.tenant,
|
||
is_active=True,
|
||
)
|
||
return super().dispatch(request, *args, **kwargs)
|
||
|
||
def get_paginate_by(self, queryset):
|
||
per_page = self.request.GET.get("per_page")
|
||
if per_page and per_page.isdigit():
|
||
return max(1, min(int(per_page), 200)) # clamp to something sane
|
||
return super().get_paginate_by(queryset)
|
||
|
||
def _parse_bool(self, value: str):
|
||
if value is None:
|
||
return None
|
||
v = value.strip().lower()
|
||
if v in {"1", "true", "yes", "on"}:
|
||
return True
|
||
if v in {"0", "false", "no", "off"}:
|
||
return False
|
||
return None
|
||
|
||
def get_queryset(self):
|
||
qs = (
|
||
InsuranceInfo.objects
|
||
.select_related("patient")
|
||
.filter(patient__tenant=self.tenant)
|
||
)
|
||
|
||
# If URL-scoped to a specific patient, enforce it
|
||
if self.patient:
|
||
qs = qs.filter(patient=self.patient)
|
||
|
||
# Query params
|
||
patient_id = self.request.GET.get("patient_id")
|
||
if patient_id and patient_id.isdigit() and not self.patient:
|
||
# Only apply if not already URL-scoped
|
||
qs = qs.filter(patient_id=patient_id)
|
||
|
||
insurance_company = self.request.GET.get("insurance_company")
|
||
if insurance_company:
|
||
qs = qs.filter(insurance_company__icontains=insurance_company.strip())
|
||
|
||
is_primary = self._parse_bool(self.request.GET.get("is_primary"))
|
||
if is_primary is True:
|
||
qs = qs.filter(is_primary=True)
|
||
elif is_primary is False:
|
||
qs = qs.filter(is_primary=False)
|
||
|
||
# Expiry status
|
||
expiry_status = self.request.GET.get("expiry_status")
|
||
today = timezone.now().date()
|
||
if expiry_status == "expired":
|
||
qs = qs.filter(expiry_date__lt=today)
|
||
elif expiry_status == "expiring_soon":
|
||
threshold = today + timedelta(days=30)
|
||
qs = qs.filter(expiry_date__range=(today, threshold))
|
||
|
||
# Optional: simple search across a few fields
|
||
search = self.request.GET.get("search")
|
||
if search:
|
||
s = search.strip()
|
||
from django.db.models import Q
|
||
qs = qs.filter(
|
||
Q(insurance_company__icontains=s)
|
||
| Q(policy_number__icontains=s)
|
||
| Q(patient__first_name__icontains=s)
|
||
| Q(patient__last_name__icontains=s)
|
||
)
|
||
|
||
return qs.order_by("patient__last_name", "patient__first_name", "-is_primary", "-effective_date")
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
# Dropdowns / helpers
|
||
ctx["patients"] = (
|
||
PatientProfile.objects
|
||
.filter(tenant=self.tenant, is_active=True)
|
||
.order_by("last_name", "first_name")
|
||
)
|
||
# Preserve current filters for the template
|
||
ctx["filters"] = {
|
||
"patient_id": self.request.GET.get("patient_id") or (self.patient.pk if self.patient else ""),
|
||
"insurance_company": self.request.GET.get("insurance_company", ""),
|
||
"is_primary": self.request.GET.get("is_primary", ""),
|
||
"expiry_status": self.request.GET.get("expiry_status", ""),
|
||
"search": self.request.GET.get("search", ""),
|
||
"per_page": self.request.GET.get("per_page", self.paginate_by),
|
||
}
|
||
# If URL-scoped patient
|
||
if self.patient:
|
||
ctx["patient"] = self.patient
|
||
return ctx
|
||
|
||
|
||
class InsuranceInfoDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Display insurance information details.
|
||
"""
|
||
model = InsuranceInfo
|
||
template_name = 'patients/insurance/insurance_detail.html'
|
||
context_object_name = 'insurance_info'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return InsuranceInfo.objects.none()
|
||
return InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
|
||
|
||
class InsuranceInfoCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
"""
|
||
Create new insurance information for a specific patient (passed as <int:pk> in the URL).
|
||
"""
|
||
model = InsuranceInfo
|
||
form_class = InsuranceInfoForm
|
||
template_name = "patients/insurance/insurance_form.html"
|
||
permission_required = "patients.add_insuranceinfo"
|
||
success_url = reverse_lazy("patients:insurance_list")
|
||
|
||
def dispatch(self, request, *args, **kwargs):
|
||
self.tenant = getattr(request, "tenant", None)
|
||
if not self.tenant:
|
||
return JsonResponse({"error": "No tenant found"}, status=400)
|
||
|
||
# patient pk comes from URL
|
||
self.patient = get_object_or_404(
|
||
PatientProfile,
|
||
pk=self.kwargs.get("pk"),
|
||
tenant=self.tenant,
|
||
is_active=True,
|
||
)
|
||
return super().dispatch(request, *args, **kwargs)
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
kwargs["user"] = self.request.user
|
||
# If the form still includes 'patient', prefill it
|
||
kwargs.setdefault("initial", {})
|
||
kwargs["initial"]["patient"] = self.patient
|
||
return kwargs
|
||
|
||
def get_context_data(self, **kwargs):
|
||
ctx = super().get_context_data(**kwargs)
|
||
ctx["patient"] = self.patient
|
||
return ctx
|
||
|
||
def form_valid(self, form):
|
||
# Always bind to the patient from the URL (ignore any posted patient)
|
||
form.instance.patient = self.patient
|
||
response = super().form_valid(form)
|
||
|
||
AuditLogger.log_event(
|
||
tenant=self.tenant,
|
||
event_type="CREATE",
|
||
event_category="PATIENT_MANAGEMENT",
|
||
action="Create Insurance Info",
|
||
description=f'Created insurance info with company "{self.object.insurance_company}" for patient {self.patient}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request,
|
||
)
|
||
|
||
messages.success(
|
||
self.request,
|
||
f'Insurance information for "{self.object.insurance_company}" created successfully.'
|
||
)
|
||
return response
|
||
|
||
# Optional: return to the patient detail (or their insurance list) instead of the global list
|
||
# def get_success_url(self):
|
||
# return reverse("patients:patient_detail", args=[self.patient.pk])
|
||
|
||
|
||
class InsuranceInfoUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
"""
|
||
Update insurance information.
|
||
"""
|
||
model = InsuranceInfo
|
||
form_class = InsuranceInfoForm
|
||
template_name = 'patients/insurance/insurance_form.html'
|
||
permission_required = 'patients.change_insuranceinfo'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return InsuranceInfo.objects.none()
|
||
return InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
kwargs['user'] = self.request.user
|
||
return kwargs
|
||
|
||
def get_success_url(self):
|
||
return reverse('patients:insurance_detail', kwargs={'pk': self.object.pk})
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
|
||
# Log insurance info update
|
||
AuditLogger.log_event(
|
||
tenant=getattr(self.request, 'tenant', None),
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Update Insurance Info',
|
||
description=f'Updated insurance info: {self.object.insurance_company}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Insurance information for "{self.object.insurance_company}" updated successfully.')
|
||
return response
|
||
|
||
|
||
class InsuranceInfoDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
"""
|
||
Delete insurance information.
|
||
"""
|
||
model = InsuranceInfo
|
||
template_name = 'patients/insurance/insurance_confirm_delete.html'
|
||
permission_required = 'patients.delete_insuranceinfo'
|
||
success_url = reverse_lazy('patients:insurance_list')
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return InsuranceInfo.objects.none()
|
||
return InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
|
||
def delete(self, request, *args, **kwargs):
|
||
self.object = self.get_object()
|
||
insurance_company = self.object.insurance_company
|
||
|
||
# Log insurance info deletion
|
||
AuditLogger.log_event(
|
||
tenant=getattr(request, 'tenant', None),
|
||
event_type='DELETE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Delete Insurance Info',
|
||
description=f'Deleted insurance info: {insurance_company}',
|
||
user=request.user,
|
||
content_object=self.object,
|
||
request=request
|
||
)
|
||
|
||
messages.success(request, f'Insurance information for "{insurance_company}" deleted successfully.')
|
||
return super().delete(request, *args, **kwargs)
|
||
|
||
|
||
class ConsentTemplateListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
List consent templates.
|
||
"""
|
||
model = ConsentTemplate
|
||
template_name = 'patients/consents/consent_template_list.html'
|
||
context_object_name = 'consent_templates'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return ConsentTemplate.objects.none()
|
||
|
||
queryset = ConsentTemplate.objects.filter(tenant=tenant)
|
||
|
||
# Apply filters
|
||
template_type = self.request.GET.get('template_type')
|
||
if template_type:
|
||
queryset = queryset.filter(template_type=template_type)
|
||
|
||
status = self.request.GET.get('status')
|
||
if status == 'active':
|
||
queryset = queryset.filter(is_active=True)
|
||
elif status == 'inactive':
|
||
queryset = queryset.filter(is_active=False)
|
||
|
||
search = self.request.GET.get('search')
|
||
if search:
|
||
queryset = queryset.filter(
|
||
Q(name__icontains=search) |
|
||
Q(description__icontains=search)
|
||
)
|
||
|
||
return queryset.order_by('template_type', 'name')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
|
||
if tenant:
|
||
context.update({
|
||
'template_types': ConsentTemplate.objects.filter(
|
||
tenant=tenant
|
||
).values_list('template_type', flat=True).distinct(),
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
class ConsentTemplateDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Display consent template details.
|
||
"""
|
||
model = ConsentTemplate
|
||
template_name = 'patients/consents/consent_template_detail.html'
|
||
context_object_name = 'consent_template'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return ConsentTemplate.objects.none()
|
||
return ConsentTemplate.objects.filter(tenant=tenant)
|
||
|
||
|
||
class ConsentTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
"""
|
||
Create new consent template.
|
||
"""
|
||
model = ConsentTemplate
|
||
form_class = ConsentTemplateForm
|
||
template_name = 'patients/consents/consent_template_form.html'
|
||
permission_required = 'patients.add_consenttemplate'
|
||
success_url = reverse_lazy('patients:consent_template_list')
|
||
|
||
def form_valid(self, form):
|
||
# Set tenant
|
||
form.instance.tenant = getattr(self.request, 'tenant', None)
|
||
response = super().form_valid(form)
|
||
|
||
# Log consent template creation
|
||
AuditLogger.log_event(
|
||
tenant=form.instance.tenant,
|
||
event_type='CREATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Create Consent Template',
|
||
description=f'Created consent template: {self.object.name}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Consent template "{self.object.name}" created successfully.')
|
||
return response
|
||
|
||
|
||
class ConsentTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
"""
|
||
Update consent template.
|
||
"""
|
||
model = ConsentTemplate
|
||
form_class = ConsentTemplateForm
|
||
template_name = 'patients/consents/consent_template_form.html'
|
||
permission_required = 'patients.change_consenttemplate'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return ConsentTemplate.objects.none()
|
||
return ConsentTemplate.objects.filter(tenant=tenant)
|
||
|
||
def get_success_url(self):
|
||
return reverse('patients:consent_template_detail', kwargs={'pk': self.object.pk})
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
|
||
# Log consent template update
|
||
AuditLogger.log_event(
|
||
tenant=self.object.tenant,
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Update Consent Template',
|
||
description=f'Updated consent template: {self.object.name}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Consent template "{self.object.name}" updated successfully.')
|
||
return response
|
||
|
||
|
||
class ConsentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
"""
|
||
Delete consent template.
|
||
"""
|
||
model = ConsentTemplate
|
||
template_name = 'patients/consents/consent_template_confirm_delete.html'
|
||
permission_required = 'patients.delete_consenttemplate'
|
||
success_url = reverse_lazy('patients:consent_template_list')
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return ConsentTemplate.objects.none()
|
||
return ConsentTemplate.objects.filter(tenant=tenant)
|
||
|
||
def delete(self, request, *args, **kwargs):
|
||
self.object = self.get_object()
|
||
template_name = self.object.name
|
||
|
||
# Check if template is being used
|
||
if ConsentForm.objects.filter(template=self.object).exists():
|
||
messages.error(request, f'Cannot delete consent template "{template_name}" as it is being used by consent forms.')
|
||
return redirect('patients:consent_template_detail', pk=self.object.pk)
|
||
|
||
# Log consent template deletion
|
||
AuditLogger.log_event(
|
||
tenant=getattr(request, 'tenant', None),
|
||
event_type='DELETE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Delete Consent Template',
|
||
description=f'Deleted consent template: {template_name}',
|
||
user=request.user,
|
||
content_object=self.object,
|
||
request=request
|
||
)
|
||
|
||
messages.success(request, f'Consent template "{template_name}" deleted successfully.')
|
||
return super().delete(request, *args, **kwargs)
|
||
|
||
|
||
class ConsentFormListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
List consent forms.
|
||
"""
|
||
model = ConsentForm
|
||
template_name = 'patients/consents/consent_form_list.html'
|
||
context_object_name = 'consent_forms'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return ConsentForm.objects.none()
|
||
|
||
queryset = ConsentForm.objects.filter(patient__tenant=tenant)
|
||
|
||
# Apply filters
|
||
patient_id = self.request.GET.get('patient_id')
|
||
if patient_id:
|
||
queryset = queryset.filter(patient_id=patient_id)
|
||
|
||
template_id = self.request.GET.get('template_id')
|
||
if template_id:
|
||
queryset = queryset.filter(template_id=template_id)
|
||
|
||
status = self.request.GET.get('status')
|
||
if status:
|
||
queryset = queryset.filter(status=status)
|
||
|
||
return queryset.order_by('-created_at')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
|
||
if tenant:
|
||
context.update({
|
||
'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'),
|
||
'templates': ConsentTemplate.objects.filter(tenant=tenant, is_active=True).order_by('name'),
|
||
'status_choices': ConsentForm.STATUS_CHOICES,
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
class ConsentFormDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Display consent form details.
|
||
"""
|
||
model = ConsentForm
|
||
template_name = 'patients/consents/consent_form_detail.html'
|
||
context_object_name = 'consent_form'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return ConsentForm.objects.none()
|
||
return ConsentForm.objects.filter(patient__tenant=tenant)
|
||
|
||
|
||
class ConsentFormCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
"""
|
||
Create new consent form.
|
||
"""
|
||
model = ConsentForm
|
||
form_class = ConsentFormForm
|
||
template_name = 'patients/consents/consent_form_form.html'
|
||
permission_required = 'patients.add_consentform'
|
||
success_url = reverse_lazy('patients:consent_form_list')
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
kwargs['user'] = self.request.user
|
||
return kwargs
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
|
||
# Log consent form creation
|
||
AuditLogger.log_event(
|
||
tenant=getattr(self.request, 'tenant', None),
|
||
event_type='CREATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Create Consent Form',
|
||
description=f'Created consent form: {self.object.template.name} for {self.object.patient}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Consent form "{self.object.template.name}" created successfully.')
|
||
return response
|
||
|
||
|
||
class ConsentFormUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
"""
|
||
Create new consent form.
|
||
"""
|
||
model = ConsentForm
|
||
form_class = ConsentFormForm
|
||
template_name = 'patients/consents/consent_form_form.html'
|
||
permission_required = 'patients.change_consentform'
|
||
success_url = reverse_lazy('patients:consent_form_list')
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
kwargs['user'] = self.request.user
|
||
return kwargs
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
|
||
# Log consent form creation
|
||
AuditLogger.log_event(
|
||
tenant=getattr(self.request, 'tenant', None),
|
||
event_type='CREATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Create Consent Form',
|
||
description=f'Created consent form: {self.object.template.name} for {self.object.patient}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Consent form "{self.object.template.name}" updated successfully.')
|
||
return response
|
||
|
||
|
||
class PatientNoteListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
List patient notes.
|
||
"""
|
||
model = PatientNote
|
||
template_name = 'patients/notes/patient_note_list.html'
|
||
context_object_name = 'patient_notes'
|
||
paginate_by = 25
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return PatientNote.objects.none()
|
||
|
||
queryset = PatientNote.objects.filter(patient__tenant=tenant)
|
||
|
||
# Apply filters
|
||
patient_id = self.request.GET.get('patient_id')
|
||
if patient_id:
|
||
queryset = queryset.filter(patient_id=patient_id)
|
||
|
||
note_type = self.request.GET.get('note_type')
|
||
if note_type:
|
||
queryset = queryset.filter(note_type=note_type)
|
||
|
||
is_confidential = self.request.GET.get('is_confidential')
|
||
if is_confidential == 'true':
|
||
queryset = queryset.filter(is_confidential=True)
|
||
elif is_confidential == 'false':
|
||
queryset = queryset.filter(is_confidential=False)
|
||
|
||
search = self.request.GET.get('search')
|
||
if search:
|
||
queryset = queryset.filter(
|
||
Q(subject__icontains=search) |
|
||
Q(content__icontains=search)
|
||
)
|
||
|
||
return queryset.order_by('-created_at')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
|
||
if tenant:
|
||
context.update({
|
||
'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'),
|
||
'note_types': PatientNote.NOTE_TYPE_CHOICES,
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
class PatientNoteDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Display patient note details.
|
||
"""
|
||
model = PatientNote
|
||
template_name = 'patients/notes/patient_note_detail.html'
|
||
context_object_name = 'patient_note'
|
||
|
||
def get_queryset(self):
|
||
tenant = getattr(self.request, 'tenant', None)
|
||
if not tenant:
|
||
return PatientNote.objects.none()
|
||
return PatientNote.objects.filter(patient__tenant=tenant)
|
||
|
||
|
||
class PatientNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
"""
|
||
Create new patient note.
|
||
"""
|
||
model = PatientNote
|
||
form_class = PatientNoteForm
|
||
template_name = 'patients/notes/patient_note_form.html'
|
||
permission_required = 'patients.add_patientnote'
|
||
success_url = reverse_lazy('patients:patient_note_list')
|
||
|
||
def get_form_kwargs(self):
|
||
kwargs = super().get_form_kwargs()
|
||
kwargs['user'] = self.request.user
|
||
return kwargs
|
||
|
||
def form_valid(self, form):
|
||
# Set author
|
||
form.instance.author = self.request.user
|
||
response = super().form_valid(form)
|
||
|
||
# Log patient note creation
|
||
AuditLogger.log_event(
|
||
tenant=getattr(self.request, 'tenant', None),
|
||
event_type='CREATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Create Patient Note',
|
||
description=f'Created patient note: {self.object.subject} for {self.object.patient}',
|
||
user=self.request.user,
|
||
content_object=self.object,
|
||
request=self.request
|
||
)
|
||
|
||
messages.success(self.request, f'Patient note "{self.object.subject}" created successfully.')
|
||
return response
|
||
|
||
|
||
@login_required
|
||
def patient_search(request):
|
||
"""
|
||
HTMX view for patient search.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
search_query = request.GET.get('search', '')
|
||
queryset = PatientProfile.objects.filter(tenant=tenant, is_active=True)
|
||
|
||
if search_query:
|
||
queryset = queryset.filter(
|
||
Q(patient_id__icontains=search_query) |
|
||
Q(first_name__icontains=search_query) |
|
||
Q(last_name__icontains=search_query) |
|
||
Q(email__icontains=search_query) |
|
||
Q(phone_number__icontains=search_query)
|
||
)
|
||
|
||
patients = queryset.order_by('last_name', 'first_name')[:20]
|
||
|
||
return render(request, 'patients/partials/patient_list.html', {
|
||
'patients': patients
|
||
})
|
||
|
||
|
||
@login_required
|
||
def patient_stats(request):
|
||
"""
|
||
HTMX view for patient statistics.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
# Calculate patient statistics
|
||
total_patients = PatientProfile.objects.filter(tenant=tenant).count()
|
||
active_patients = PatientProfile.objects.filter(tenant=tenant, is_active=True).count()
|
||
|
||
# Age distribution
|
||
today = timezone.now().date()
|
||
pediatric_count = PatientProfile.objects.filter(
|
||
tenant=tenant,
|
||
is_active=True,
|
||
date_of_birth__gt=today.replace(year=today.year - 18)
|
||
).count()
|
||
|
||
adult_count = PatientProfile.objects.filter(
|
||
tenant=tenant,
|
||
is_active=True,
|
||
date_of_birth__lte=today.replace(year=today.year - 18),
|
||
date_of_birth__gt=today.replace(year=today.year - 65)
|
||
).count()
|
||
|
||
geriatric_count = PatientProfile.objects.filter(
|
||
tenant=tenant,
|
||
is_active=True,
|
||
date_of_birth__lte=today.replace(year=today.year - 65)
|
||
).count()
|
||
|
||
# Gender distribution
|
||
gender_stats = PatientProfile.objects.filter(tenant=tenant, is_active=True).values('gender').annotate(
|
||
count=Count('id')
|
||
).order_by('-count')
|
||
|
||
# Recent registrations (last 30 days)
|
||
thirty_days_ago = today - timedelta(days=30)
|
||
recent_registrations = PatientProfile.objects.filter(
|
||
tenant=tenant,
|
||
created_at__gte=thirty_days_ago
|
||
).count()
|
||
|
||
stats = {
|
||
'total_patients': total_patients,
|
||
'active_patients': active_patients,
|
||
'pediatric_count': pediatric_count,
|
||
'adult_count': adult_count,
|
||
'geriatric_count': geriatric_count,
|
||
'gender_stats': gender_stats,
|
||
'recent_registrations': recent_registrations,
|
||
}
|
||
|
||
return render(request, 'patients/partials/patient_stats.html', {'stats': stats})
|
||
|
||
|
||
@login_required
|
||
def emergency_contacts_list(request, patient_id):
|
||
"""
|
||
HTMX view for emergency contacts list.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
patient = get_object_or_404(PatientProfile, patient_id=patient_id, tenant=tenant)
|
||
emergency_contacts = EmergencyContact.objects.filter(
|
||
patient=patient
|
||
).order_by('-is_primary', 'first_name')
|
||
|
||
return render(request, 'patients/partials/emergency_contacts_list.html', {
|
||
'emergency_contacts': emergency_contacts,
|
||
'patient': patient
|
||
})
|
||
|
||
|
||
@login_required
|
||
def insurance_info_list(request, patient_id):
|
||
"""
|
||
HTMX view for insurance info list.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
insurance_info = InsuranceInfo.objects.filter(
|
||
patient=patient
|
||
).order_by('-effective_date')
|
||
|
||
return render(request, 'patients/partials/insurance_info_list.html', {
|
||
'insurance_info': insurance_info,
|
||
'patient': patient
|
||
})
|
||
|
||
|
||
@login_required
|
||
def consent_forms_list(request, patient_id):
|
||
"""
|
||
HTMX view for consent forms list.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
consent_forms = ConsentForm.objects.filter(
|
||
patient=patient
|
||
).order_by('-created_at')
|
||
|
||
return render(request, 'patients/partials/consent_forms_list.html', {
|
||
'consent_forms': consent_forms,
|
||
'patient': patient
|
||
})
|
||
|
||
|
||
@login_required
|
||
def patient_notes_list(request, patient_id):
|
||
"""
|
||
HTMX view for patient notes list.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
patient_notes = PatientNote.objects.filter(
|
||
patient=patient
|
||
).order_by('-created_at')[:10]
|
||
|
||
return render(request, 'patients/partials/patient_notes_list.html', {
|
||
'patient_notes': patient_notes,
|
||
'patient': patient
|
||
})
|
||
|
||
|
||
def patient_appointment_list(request, pk):
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
patient = get_object_or_404(PatientProfile, id=pk, tenant=tenant)
|
||
patient_appointments = AppointmentRequest.objects.filter(
|
||
patient=patient
|
||
).order_by('-created_at')
|
||
|
||
return render(request, 'patients/partials/patient_appointment_list.html', {
|
||
'patient_appointments': patient_appointments,
|
||
'patient': patient
|
||
})
|
||
|
||
# ============================================================================
|
||
# ACTION VIEWS FOR WORKFLOW OPERATIONS
|
||
# ============================================================================
|
||
|
||
@login_required
|
||
def activate_patient(request, pk):
|
||
"""
|
||
Activate a patient account.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
messages.error(request, 'No tenant found.')
|
||
return redirect('patients:patient_list')
|
||
|
||
patient = get_object_or_404(PatientProfile, pk=pk, tenant=tenant)
|
||
|
||
patient.is_active = True
|
||
patient.save()
|
||
|
||
# Log activation
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Activate Patient',
|
||
description=f'Activated patient: {patient.first_name} {patient.last_name}',
|
||
user=request.user,
|
||
content_object=patient,
|
||
request=request
|
||
)
|
||
|
||
messages.success(request, f'Patient "{patient.first_name} {patient.last_name}" activated successfully.')
|
||
return redirect('patients:patient_detail', pk=pk)
|
||
|
||
|
||
@login_required
|
||
def sign_consent_form(request, pk):
|
||
"""
|
||
Sign a consent form.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
consent_form = get_object_or_404(
|
||
ConsentForm,
|
||
pk=pk,
|
||
patient__tenant=tenant
|
||
)
|
||
|
||
if request.method == 'POST':
|
||
# Update consent form status
|
||
consent_form.status = 'SIGNED'
|
||
consent_form.signed_date = timezone.now()
|
||
consent_form.signed_by = request.user
|
||
|
||
# Add witness information if required
|
||
if consent_form.template.requires_witness:
|
||
consent_form.witness_name = request.POST.get('witness_name')
|
||
consent_form.witness_signature = request.POST.get('witness_signature')
|
||
|
||
consent_form.save()
|
||
|
||
# Log consent form signing
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Sign Consent Form',
|
||
description=f'Signed consent form: {consent_form.template.name} for {consent_form.patient}',
|
||
user=request.user,
|
||
content_object=consent_form,
|
||
request=request
|
||
)
|
||
|
||
messages.success(request, f'Consent form "{consent_form.template.name}" signed successfully.')
|
||
return JsonResponse({'status': 'signed'})
|
||
|
||
return render(request, 'patients/partials/sign_consent_form.html', {
|
||
'consent_form': consent_form
|
||
})
|
||
|
||
|
||
@login_required
|
||
def add_patient_note(request, patient_id):
|
||
"""
|
||
HTMX view to add a patient note.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
|
||
if request.method == 'POST':
|
||
form = PatientNoteForm(request.POST, user=request.user)
|
||
if form.is_valid():
|
||
note = form.save(commit=False)
|
||
note.patient = patient
|
||
note.author = request.user
|
||
note.save()
|
||
|
||
# Log patient note creation
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='CREATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Add Patient Note',
|
||
description=f'Added patient note: {note.subject} for {patient}',
|
||
user=request.user,
|
||
content_object=note,
|
||
request=request
|
||
)
|
||
|
||
return JsonResponse({'status': 'created', 'note_id': note.id})
|
||
else:
|
||
form = PatientNoteForm(user=request.user, initial={'patient': patient})
|
||
|
||
return render(request, 'patients/partials/add_patient_note.html', {
|
||
'form': form,
|
||
'patient': patient
|
||
})
|
||
|
||
|
||
def get_patient_info(request, pk):
|
||
patient = get_object_or_404(PatientProfile, pk=pk)
|
||
patient_info = {
|
||
'id': patient.id,
|
||
'first_name': patient.first_name,
|
||
'last_name': patient.last_name,
|
||
'gender': patient.get_gender_display(),
|
||
'date_of_birth': patient.date_of_birth,
|
||
'phone_number': patient.phone_number,
|
||
'email': patient.email,
|
||
}
|
||
return JsonResponse(patient_info)
|
||
|
||
#
|
||
# """
|
||
# Patients app views for hospital management system with comprehensive CRUD operations.
|
||
# """
|
||
#
|
||
# 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, PermissionRequiredMixin
|
||
# from django.views.generic import (
|
||
# TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
|
||
# )
|
||
# from django.http import JsonResponse
|
||
# from django.contrib import messages
|
||
# from django.db.models import Q, Count
|
||
# from django.utils import timezone
|
||
# from django.urls import reverse_lazy, reverse
|
||
# from django.core.paginator import Paginator
|
||
# from datetime import timedelta, date
|
||
#
|
||
# from core.models import AuditLogEntry
|
||
# from .models import *
|
||
# from .forms import *
|
||
# from core.utils import AuditLogger
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # PATIENT PROFILE VIEWS (FULL CRUD - Master Data)
|
||
# # ============================================================================
|
||
#
|
||
# class PatientListView(LoginRequiredMixin, ListView):
|
||
# """
|
||
# Patient listing view.
|
||
# """
|
||
# model = PatientProfile
|
||
# template_name = 'patients/patient_list.html'
|
||
# context_object_name = 'patients'
|
||
# paginate_by = 25
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return PatientProfile.objects.none()
|
||
#
|
||
# queryset = PatientProfile.objects.filter(tenant=tenant)
|
||
#
|
||
# # Apply filters
|
||
# gender = self.request.GET.get('gender')
|
||
# if gender:
|
||
# queryset = queryset.filter(gender=gender)
|
||
#
|
||
# age_range = self.request.GET.get('age_range')
|
||
# if age_range:
|
||
# today = timezone.now().date()
|
||
# if age_range == 'pediatric':
|
||
# # Under 18
|
||
# min_birth_date = today.replace(year=today.year - 18)
|
||
# queryset = queryset.filter(date_of_birth__gt=min_birth_date)
|
||
# elif age_range == 'adult':
|
||
# # 18-64
|
||
# min_birth_date = today.replace(year=today.year - 65)
|
||
# max_birth_date = today.replace(year=today.year - 18)
|
||
# queryset = queryset.filter(
|
||
# date_of_birth__gt=min_birth_date,
|
||
# date_of_birth__lte=max_birth_date
|
||
# )
|
||
# elif age_range == 'geriatric':
|
||
# # 65+
|
||
# max_birth_date = today.replace(year=today.year - 65)
|
||
# queryset = queryset.filter(date_of_birth__lte=max_birth_date)
|
||
#
|
||
# blood_type = self.request.GET.get('blood_type')
|
||
# if blood_type:
|
||
# queryset = queryset.filter(blood_type=blood_type)
|
||
#
|
||
# status = self.request.GET.get('status')
|
||
# if status == 'active':
|
||
# queryset = queryset.filter(is_active=True)
|
||
# elif status == 'inactive':
|
||
# queryset = queryset.filter(is_active=False)
|
||
#
|
||
# search = self.request.GET.get('search')
|
||
# if search:
|
||
# queryset = queryset.filter(
|
||
# Q(patient_id__icontains=search) |
|
||
# Q(first_name__icontains=search) |
|
||
# Q(last_name__icontains=search) |
|
||
# Q(email__icontains=search) |
|
||
# Q(phone_number__icontains=search) |
|
||
# Q(mobile_number__icontains=search)
|
||
# )
|
||
#
|
||
# return queryset.order_by('last_name', 'first_name')
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
#
|
||
# if tenant:
|
||
# context.update({
|
||
# 'search_form': PatientsSearchForm(self.request.GET),
|
||
# 'total_patients': PatientProfile.objects.filter(tenant=tenant).count(),
|
||
# 'active_patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).count(),
|
||
# })
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
# class PatientDetailView(LoginRequiredMixin, DetailView):
|
||
# """
|
||
# Patient detail view.
|
||
# """
|
||
# model = PatientProfile
|
||
# template_name = 'patients/patient_detail.html'
|
||
# context_object_name = 'patient'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return PatientProfile.objects.none()
|
||
# return PatientProfile.objects.filter(tenant=tenant)
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# patient = self.get_object()
|
||
#
|
||
# # Get related data
|
||
# context['emergency_contacts'] = EmergencyContact.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_primary', 'name')
|
||
#
|
||
# context['insurance_info'] = InsuranceInfo.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_primary', '-effective_date')
|
||
#
|
||
# context['consent_forms'] = ConsentForm.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-created_at')
|
||
#
|
||
# context['patient_notes'] = PatientNote.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-created_at')[:10]
|
||
#
|
||
# # Calculate age
|
||
# if patient.date_of_birth:
|
||
# today = date.today()
|
||
# age = today.year - patient.date_of_birth.year
|
||
# if today.month < patient.date_of_birth.month or \
|
||
# (today.month == patient.date_of_birth.month and today.day < patient.date_of_birth.day):
|
||
# age -= 1
|
||
# context['patient_age'] = age
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
# class PatientRegistrationView(LoginRequiredMixin, CreateView):
|
||
# """
|
||
# View for registering new patients with comprehensive information.
|
||
# """
|
||
# model = PatientProfile
|
||
# form_class = PatientRegistrationForm
|
||
# template_name = 'patients/patient_registration.html'
|
||
# success_url = reverse_lazy('patients:patient_list')
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# """Add user and tenant to form kwargs"""
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# kwargs['tenant'] = getattr(self.request, 'tenant', None)
|
||
# return kwargs
|
||
#
|
||
# def form_valid(self, form):
|
||
# """Process valid form and handle the related objects"""
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Access the related objects created during form save
|
||
# if hasattr(form, 'emergency_contacts'):
|
||
# messages.success(self.request, 'Emergency contact information saved.')
|
||
#
|
||
# if hasattr(form, 'insurance_info'):
|
||
# messages.success(self.request, 'Insurance information saved.')
|
||
#
|
||
# messages.success(self.request, f'Patient {self.object.get_full_name()} registered successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class PatientCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
# """
|
||
# Create new patient.
|
||
# """
|
||
# model = PatientProfile
|
||
# form_class = PatientProfileForm
|
||
# template_name = 'patients/patient_create.html'
|
||
# permission_required = 'patients.add_patientprofile'
|
||
# success_url = reverse_lazy('patients:patient_list')
|
||
#
|
||
# def form_valid(self, form):
|
||
# # Set tenant
|
||
# form.instance.tenant = getattr(self.request, 'tenant', None)
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log patient creation
|
||
# AuditLogger.log_event(
|
||
# tenant=form.instance.tenant,
|
||
# event_type='CREATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Create Patient',
|
||
# description=f'Created new patient: {self.object.first_name} {self.object.last_name}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request,
|
||
# f'Patient "{self.object.first_name} {self.object.last_name}" created successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class PatientUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
# """
|
||
# Update patient information.
|
||
# """
|
||
# model = PatientProfile
|
||
# form_class = PatientProfileForm
|
||
# template_name = 'patients/patient_form.html'
|
||
# permission_required = 'patients.change_patientprofile'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return PatientProfile.objects.none()
|
||
# return PatientProfile.objects.filter(tenant=tenant)
|
||
#
|
||
# def get_success_url(self):
|
||
# return reverse('patients:patient_detail', kwargs={'pk': self.object.pk})
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log patient update
|
||
# AuditLogger.log_event(
|
||
# tenant=self.object.tenant,
|
||
# event_type='UPDATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Update Patient',
|
||
# description=f'Updated patient: {self.object.first_name} {self.object.last_name}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request,
|
||
# f'Patient "{self.object.first_name} {self.object.last_name}" updated successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class PatientDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
# """
|
||
# Delete patient (soft delete to inactive).
|
||
# """
|
||
# model = PatientProfile
|
||
# template_name = 'patients/patient_confirm_delete.html'
|
||
# permission_required = 'patients.delete_patientprofile'
|
||
# success_url = reverse_lazy('patients:patient_list')
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return PatientProfile.objects.none()
|
||
# return PatientProfile.objects.filter(tenant=tenant)
|
||
#
|
||
# def delete(self, request, *args, **kwargs):
|
||
# self.object = self.get_object()
|
||
#
|
||
# # Soft delete - set to inactive
|
||
# self.object.is_active = False
|
||
# self.object.save()
|
||
#
|
||
# # Log patient deletion
|
||
# AuditLogger.log_event(
|
||
# tenant=self.object.tenant,
|
||
# event_type='DELETE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Deactivate Patient',
|
||
# description=f'Deactivated patient: {self.object.first_name} {self.object.last_name}',
|
||
# user=request.user,
|
||
# content_object=self.object,
|
||
# request=request
|
||
# )
|
||
#
|
||
# messages.success(request,
|
||
# f'Patient "{self.object.first_name} {self.object.last_name}" deactivated successfully.')
|
||
# return redirect(self.success_url)
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # EMERGENCY CONTACT VIEWS (FULL CRUD - Operational Data)
|
||
# # ============================================================================
|
||
#
|
||
# class EmergencyContactListView(LoginRequiredMixin, ListView):
|
||
# """
|
||
# List emergency contacts.
|
||
# """
|
||
# model = EmergencyContact
|
||
# template_name = 'patients/emergency_contact_list.html'
|
||
# context_object_name = 'emergency_contacts'
|
||
# paginate_by = 25
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return EmergencyContact.objects.none()
|
||
#
|
||
# queryset = EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# # Apply filters
|
||
# patient_id = self.request.GET.get('patient_id')
|
||
# if patient_id:
|
||
# queryset = queryset.filter(patient_id=patient_id)
|
||
#
|
||
# relationship = self.request.GET.get('relationship')
|
||
# if relationship:
|
||
# queryset = queryset.filter(relationship__icontains=relationship)
|
||
#
|
||
# is_primary = self.request.GET.get('is_primary')
|
||
# if is_primary == 'true':
|
||
# queryset = queryset.filter(is_primary=True)
|
||
# elif is_primary == 'false':
|
||
# queryset = queryset.filter(is_primary=False)
|
||
#
|
||
# return queryset.order_by('patient__last_name', '-is_primary', 'name')
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
#
|
||
# if tenant:
|
||
# context.update({
|
||
# 'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name',
|
||
# 'first_name'),
|
||
# })
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
# class EmergencyContactDetailView(LoginRequiredMixin, DetailView):
|
||
# """
|
||
# Display emergency contact details.
|
||
# """
|
||
# model = EmergencyContact
|
||
# template_name = 'patients/emergency_contact_detail.html'
|
||
# context_object_name = 'emergency_contact'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return EmergencyContact.objects.none()
|
||
# return EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
#
|
||
#
|
||
# class EmergencyContactCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
# """
|
||
# Create new emergency contact.
|
||
# """
|
||
# model = EmergencyContact
|
||
# form_class = EmergencyContactForm
|
||
# template_name = 'patients/emergency_contact_form.html'
|
||
# permission_required = 'patients.add_emergencycontact'
|
||
# success_url = reverse_lazy('patients:emergency_contact_list')
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# return kwargs
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log emergency contact creation
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(self.request, 'tenant', None),
|
||
# event_type='CREATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Create Emergency Contact',
|
||
# description=f'Created emergency contact: {self.object.name} for {self.object.patient}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request, f'Emergency contact "{self.object.name}" created successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class EmergencyContactUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
# """
|
||
# Update emergency contact.
|
||
# """
|
||
# model = EmergencyContact
|
||
# form_class = EmergencyContactForm
|
||
# template_name = 'patients/emergency_contact_form.html'
|
||
# permission_required = 'patients.change_emergencycontact'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return EmergencyContact.objects.none()
|
||
# return EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# return kwargs
|
||
#
|
||
# def get_success_url(self):
|
||
# return reverse('patients:emergency_contact_detail', kwargs={'pk': self.object.pk})
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log emergency contact update
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(self.request, 'tenant', None),
|
||
# event_type='UPDATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Update Emergency Contact',
|
||
# description=f'Updated emergency contact: {self.object.name}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request, f'Emergency contact "{self.object.name}" updated successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class EmergencyContactDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
# """
|
||
# Delete emergency contact.
|
||
# """
|
||
# model = EmergencyContact
|
||
# template_name = 'patients/emergency_contact_confirm_delete.html'
|
||
# permission_required = 'patients.delete_emergencycontact'
|
||
# success_url = reverse_lazy('patients:emergency_contact_list')
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return EmergencyContact.objects.none()
|
||
# return EmergencyContact.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# def delete(self, request, *args, **kwargs):
|
||
# self.object = self.get_object()
|
||
# contact_name = self.object.name
|
||
#
|
||
# # Log emergency contact deletion
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(request, 'tenant', None),
|
||
# event_type='DELETE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Delete Emergency Contact',
|
||
# description=f'Deleted emergency contact: {contact_name}',
|
||
# user=request.user,
|
||
# content_object=self.object,
|
||
# request=request
|
||
# )
|
||
#
|
||
# messages.success(request, f'Emergency contact "{contact_name}" deleted successfully.')
|
||
# return super().delete(request, *args, **kwargs)
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # INSURANCE INFO VIEWS (FULL CRUD - Operational Data)
|
||
# # ============================================================================
|
||
#
|
||
# class InsuranceInfoListView(LoginRequiredMixin, ListView):
|
||
# """
|
||
# List insurance information.
|
||
# """
|
||
# model = InsuranceInfo
|
||
# template_name = 'patients/insurance_info_list.html'
|
||
# context_object_name = 'insurance_info'
|
||
# paginate_by = 25
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return InsuranceInfo.objects.none()
|
||
#
|
||
# queryset = InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# # Apply filters
|
||
# patient_id = self.request.GET.get('patient_id')
|
||
# if patient_id:
|
||
# queryset = queryset.filter(patient_id=patient_id)
|
||
#
|
||
# insurance_company = self.request.GET.get('insurance_company')
|
||
# if insurance_company:
|
||
# queryset = queryset.filter(insurance_company__icontains=insurance_company)
|
||
#
|
||
# is_primary = self.request.GET.get('is_primary')
|
||
# if is_primary == 'true':
|
||
# queryset = queryset.filter(is_primary=True)
|
||
# elif is_primary == 'false':
|
||
# queryset = queryset.filter(is_primary=False)
|
||
#
|
||
# # Filter by expiry status
|
||
# expiry_status = self.request.GET.get('expiry_status')
|
||
# today = timezone.now().date()
|
||
# if expiry_status == 'expired':
|
||
# queryset = queryset.filter(expiry_date__lt=today)
|
||
# elif expiry_status == 'expiring_soon':
|
||
# # Expiring within 30 days
|
||
# expiry_threshold = today + timedelta(days=30)
|
||
# queryset = queryset.filter(expiry_date__lte=expiry_threshold, expiry_date__gte=today)
|
||
#
|
||
# return queryset.order_by('patient__last_name', '-is_primary', '-effective_date')
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
#
|
||
# if tenant:
|
||
# context.update({
|
||
# 'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name',
|
||
# 'first_name'),
|
||
# })
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
# class InsuranceInfoDetailView(LoginRequiredMixin, DetailView):
|
||
# """
|
||
# Display insurance information details.
|
||
# """
|
||
# model = InsuranceInfo
|
||
# template_name = 'patients/insurance_info_detail.html'
|
||
# context_object_name = 'insurance_info'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return InsuranceInfo.objects.none()
|
||
# return InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
#
|
||
#
|
||
# class InsuranceInfoCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
# """
|
||
# Create new insurance information.
|
||
# """
|
||
# model = InsuranceInfo
|
||
# form_class = InsuranceInfoForm
|
||
# template_name = 'patients/insurance_info_form.html'
|
||
# permission_required = 'patients.add_insuranceinfo'
|
||
# success_url = reverse_lazy('patients:insurance_info_list')
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# return kwargs
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log insurance info creation
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(self.request, 'tenant', None),
|
||
# event_type='CREATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Create Insurance Info',
|
||
# description=f'Created insurance info: {self.object.insurance_company} for {self.object.patient}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request,
|
||
# f'Insurance information for "{self.object.insurance_company}" created successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class InsuranceInfoUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
# """
|
||
# Update insurance information.
|
||
# """
|
||
# model = InsuranceInfo
|
||
# form_class = InsuranceInfoForm
|
||
# template_name = 'patients/insurance_info_form.html'
|
||
# permission_required = 'patients.change_insuranceinfo'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return InsuranceInfo.objects.none()
|
||
# return InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# return kwargs
|
||
#
|
||
# def get_success_url(self):
|
||
# return reverse('patients:insurance_info_detail', kwargs={'pk': self.object.pk})
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log insurance info update
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(self.request, 'tenant', None),
|
||
# event_type='UPDATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Update Insurance Info',
|
||
# description=f'Updated insurance info: {self.object.insurance_company}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request,
|
||
# f'Insurance information for "{self.object.insurance_company}" updated successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class InsuranceInfoDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
# """
|
||
# Delete insurance information.
|
||
# """
|
||
# model = InsuranceInfo
|
||
# template_name = 'patients/insurance_info_confirm_delete.html'
|
||
# permission_required = 'patients.delete_insuranceinfo'
|
||
# success_url = reverse_lazy('patients:insurance_info_list')
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return InsuranceInfo.objects.none()
|
||
# return InsuranceInfo.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# def delete(self, request, *args, **kwargs):
|
||
# self.object = self.get_object()
|
||
# insurance_company = self.object.insurance_company
|
||
#
|
||
# # Log insurance info deletion
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(request, 'tenant', None),
|
||
# event_type='DELETE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Delete Insurance Info',
|
||
# description=f'Deleted insurance info: {insurance_company}',
|
||
# user=request.user,
|
||
# content_object=self.object,
|
||
# request=request
|
||
# )
|
||
#
|
||
# messages.success(request, f'Insurance information for "{insurance_company}" deleted successfully.')
|
||
# return super().delete(request, *args, **kwargs)
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # CONSENT TEMPLATE VIEWS (FULL CRUD - Master Data)
|
||
# # ============================================================================
|
||
#
|
||
# class ConsentTemplateListView(LoginRequiredMixin, ListView):
|
||
# """
|
||
# List consent templates.
|
||
# """
|
||
# model = ConsentTemplate
|
||
# template_name = 'patients/consent_template_list.html'
|
||
# context_object_name = 'consent_templates'
|
||
# paginate_by = 25
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return ConsentTemplate.objects.none()
|
||
#
|
||
# queryset = ConsentTemplate.objects.filter(tenant=tenant)
|
||
#
|
||
# # Apply filters
|
||
# template_type = self.request.GET.get('template_type')
|
||
# if template_type:
|
||
# queryset = queryset.filter(template_type=template_type)
|
||
#
|
||
# status = self.request.GET.get('status')
|
||
# if status == 'active':
|
||
# queryset = queryset.filter(is_active=True)
|
||
# elif status == 'inactive':
|
||
# queryset = queryset.filter(is_active=False)
|
||
#
|
||
# search = self.request.GET.get('search')
|
||
# if search:
|
||
# queryset = queryset.filter(
|
||
# Q(name__icontains=search) |
|
||
# Q(description__icontains=search)
|
||
# )
|
||
#
|
||
# return queryset.order_by('template_type', 'name')
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
#
|
||
# if tenant:
|
||
# context.update({
|
||
# 'template_types': ConsentTemplate.objects.filter(
|
||
# tenant=tenant
|
||
# ).values_list('template_type', flat=True).distinct(),
|
||
# })
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
# class ConsentTemplateDetailView(LoginRequiredMixin, DetailView):
|
||
# """
|
||
# Display consent template details.
|
||
# """
|
||
# model = ConsentTemplate
|
||
# template_name = 'patients/consent_template_detail.html'
|
||
# context_object_name = 'consent_template'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return ConsentTemplate.objects.none()
|
||
# return ConsentTemplate.objects.filter(tenant=tenant)
|
||
#
|
||
#
|
||
# class ConsentTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
# """
|
||
# Create new consent template.
|
||
# """
|
||
# model = ConsentTemplate
|
||
# form_class = ConsentTemplateForm
|
||
# template_name = 'patients/consent_template_form.html'
|
||
# permission_required = 'patients.add_consenttemplate'
|
||
# success_url = reverse_lazy('patients:consent_template_list')
|
||
#
|
||
# def form_valid(self, form):
|
||
# # Set tenant
|
||
# form.instance.tenant = getattr(self.request, 'tenant', None)
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log consent template creation
|
||
# AuditLogger.log_event(
|
||
# tenant=form.instance.tenant,
|
||
# event_type='CREATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Create Consent Template',
|
||
# description=f'Created consent template: {self.object.name}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request, f'Consent template "{self.object.name}" created successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class ConsentTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||
# """
|
||
# Update consent template.
|
||
# """
|
||
# model = ConsentTemplate
|
||
# form_class = ConsentTemplateForm
|
||
# template_name = 'patients/consent_template_form.html'
|
||
# permission_required = 'patients.change_consenttemplate'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return ConsentTemplate.objects.none()
|
||
# return ConsentTemplate.objects.filter(tenant=tenant)
|
||
#
|
||
# def get_success_url(self):
|
||
# return reverse('patients:consent_template_detail', kwargs={'pk': self.object.pk})
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log consent template update
|
||
# AuditLogger.log_event(
|
||
# tenant=self.object.tenant,
|
||
# event_type='UPDATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Update Consent Template',
|
||
# description=f'Updated consent template: {self.object.name}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request, f'Consent template "{self.object.name}" updated successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# class ConsentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||
# """
|
||
# Delete consent template.
|
||
# """
|
||
# model = ConsentTemplate
|
||
# template_name = 'patients/consent_template_confirm_delete.html'
|
||
# permission_required = 'patients.delete_consenttemplate'
|
||
# success_url = reverse_lazy('patients:consent_template_list')
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return ConsentTemplate.objects.none()
|
||
# return ConsentTemplate.objects.filter(tenant=tenant)
|
||
#
|
||
# def delete(self, request, *args, **kwargs):
|
||
# self.object = self.get_object()
|
||
# template_name = self.object.name
|
||
#
|
||
# # Check if template is being used
|
||
# if ConsentForm.objects.filter(template=self.object).exists():
|
||
# messages.error(request,
|
||
# f'Cannot delete consent template "{template_name}" as it is being used by consent forms.')
|
||
# return redirect('patients:consent_template_detail', pk=self.object.pk)
|
||
#
|
||
# # Log consent template deletion
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(request, 'tenant', None),
|
||
# event_type='DELETE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Delete Consent Template',
|
||
# description=f'Deleted consent template: {template_name}',
|
||
# user=request.user,
|
||
# content_object=self.object,
|
||
# request=request
|
||
# )
|
||
#
|
||
# messages.success(request, f'Consent template "{template_name}" deleted successfully.')
|
||
# return super().delete(request, *args, **kwargs)
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # CONSENT FORM VIEWS (APPEND-ONLY - Clinical Records)
|
||
# # ============================================================================
|
||
#
|
||
# class ConsentFormListView(LoginRequiredMixin, ListView):
|
||
# """
|
||
# List consent forms.
|
||
# """
|
||
# model = ConsentForm
|
||
# template_name = 'patients/consent_form_list.html'
|
||
# context_object_name = 'consent_forms'
|
||
# paginate_by = 25
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return ConsentForm.objects.none()
|
||
#
|
||
# queryset = ConsentForm.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# # Apply filters
|
||
# patient_id = self.request.GET.get('patient_id')
|
||
# if patient_id:
|
||
# queryset = queryset.filter(patient_id=patient_id)
|
||
#
|
||
# template_id = self.request.GET.get('template_id')
|
||
# if template_id:
|
||
# queryset = queryset.filter(template_id=template_id)
|
||
#
|
||
# status = self.request.GET.get('status')
|
||
# if status:
|
||
# queryset = queryset.filter(status=status)
|
||
#
|
||
# return queryset.order_by('-created_at')
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
#
|
||
# if tenant:
|
||
# context.update({
|
||
# 'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name',
|
||
# 'first_name'),
|
||
# 'templates': ConsentTemplate.objects.filter(tenant=tenant, is_active=True).order_by('name'),
|
||
# 'status_choices': ConsentForm.STATUS_CHOICES,
|
||
# })
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
class ConsentManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
|
||
"""
|
||
View for managing consent templates and patient consent forms.
|
||
|
||
This view provides functionality to:
|
||
1. Create and manage consent templates
|
||
2. View consent status across patients
|
||
3. Generate consent forms from templates
|
||
4. Track consent expirations
|
||
"""
|
||
template_name = 'patients/consent_management.html'
|
||
permission_required = 'patients.view_consenttemplate'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
"""Prepare comprehensive context for consent management"""
|
||
context = super().get_context_data(**kwargs)
|
||
|
||
# Get active consent templates
|
||
templates = ConsentTemplate.objects.filter(
|
||
tenant=self.request.user.tenant,
|
||
is_active=True
|
||
).order_by('category', 'name')
|
||
|
||
# Count consents by category
|
||
category_counts = ConsentTemplate.objects.filter(
|
||
tenant=self.request.user.tenant
|
||
).values('category').annotate(count=Count('id'))
|
||
|
||
# Get recent consent forms
|
||
recent_consents = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant
|
||
).select_related('patient', 'template').order_by('-created_at')
|
||
|
||
# consent_forms = ConsentForm.objects.filter(
|
||
# patient__tenant=self.request.user.tenant
|
||
# )
|
||
|
||
# Get expired/expiring consents
|
||
today = timezone.now().date()
|
||
expiry_threshold = today + timedelta(days=30)
|
||
|
||
expiring_consents = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant,
|
||
status='SIGNED',
|
||
expiry_date__lte=expiry_threshold,
|
||
expiry_date__gt=today
|
||
).select_related('patient', 'template').order_by('expiry_date')
|
||
|
||
expired_consents = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant,
|
||
status='SIGNED',
|
||
expiry_date__lt=today
|
||
).select_related('patient', 'template').order_by('-expiry_date')[:20]
|
||
|
||
# Get unsigned consents
|
||
unsigned_consents = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant,
|
||
status='PENDING'
|
||
).select_related('patient', 'template').order_by('-created_at')[:20]
|
||
|
||
# Calculate consent statistics
|
||
total_consents = ConsentForm.objects.filter(patient__tenant=self.request.user.tenant).count()
|
||
signed_count = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant,
|
||
status='SIGNED'
|
||
).count()
|
||
|
||
expired_count = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant,
|
||
status='SIGNED',
|
||
expiry_date__lt=today
|
||
).count()
|
||
|
||
pending_count = ConsentForm.objects.filter(
|
||
patient__tenant=self.request.user.tenant,
|
||
status='PENDING'
|
||
).count()
|
||
|
||
# Check if we have a template to create
|
||
template_form = None
|
||
if self.request.user.has_perm('patients.add_consenttemplate'):
|
||
template_form = ConsentTemplateForm
|
||
|
||
# Prepare context
|
||
context.update({
|
||
'title': 'Consent Management',
|
||
'templates': templates,
|
||
'category_counts': category_counts,
|
||
'consents': recent_consents,
|
||
'expiring_consents': expiring_consents,
|
||
'expired_consents': expired_consents,
|
||
'unsigned_consents': unsigned_consents,
|
||
'total_consents': total_consents,
|
||
'signed_count': signed_count,
|
||
'expired_count': expired_count,
|
||
'pending_count': pending_count,
|
||
'template_form': template_form,
|
||
'consent_categories': ConsentTemplate._meta.get_field('category').choices,
|
||
})
|
||
|
||
return context
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Handle form submissions for template creation"""
|
||
# Check permission for template creation
|
||
if not request.user.has_perm('patients.add_consenttemplate'):
|
||
messages.error(request, "You don't have permission to create consent templates.")
|
||
return redirect('patients:consent_management')
|
||
|
||
# Process template form
|
||
template_form = ConsentTemplateForm
|
||
|
||
if template_form.is_valid:
|
||
template = template_form.save
|
||
messages.success(
|
||
request,
|
||
f"Consent template '{template.name}' created successfully."
|
||
)
|
||
return redirect('patients:consent_management')
|
||
else:
|
||
# Re-render page with form errors
|
||
context = self.get_context_data()
|
||
context['template_form'] = template_form
|
||
return self.render_to_response(context)
|
||
|
||
def get_template(self, template_id):
|
||
"""Get a consent template by ID"""
|
||
return get_object_or_404(
|
||
ConsentTemplate,
|
||
pk=template_id,
|
||
tenant=self.request.user.tenant
|
||
)
|
||
|
||
def generate_consent_form(self, patient_id, template_id):
|
||
"""Generate a consent form for a patient from a template"""
|
||
# Validate permissions
|
||
if not self.request.user.has_perm('patients.add_consentform'):
|
||
messages.error(self.request, "You don't have permission to generate consent forms.")
|
||
return redirect('patients:consent_management')
|
||
|
||
# Get the patient and template
|
||
patient = get_object_or_404(
|
||
PatientProfile,
|
||
pk=patient_id,
|
||
tenant=self.request.user.tenant
|
||
)
|
||
|
||
template = self.get_template(template_id)
|
||
|
||
# Check if a consent form already exists
|
||
existing_form = ConsentForm.objects.filter(
|
||
patient=patient,
|
||
template=template,
|
||
status='PENDING'
|
||
).first()
|
||
|
||
if existing_form:
|
||
messages.info(
|
||
self.request,
|
||
f"A pending consent form already exists for {patient.get_full_name()}."
|
||
)
|
||
return redirect('patients:consent_form_detail', pk=existing_form.pk)
|
||
|
||
# Create the consent form
|
||
consent_form = ConsentForm.objects.create(
|
||
consent_id=uuid.uuid4(),
|
||
patient=patient,
|
||
template=template,
|
||
tenant=self.request.user.tenant,
|
||
status='PENDING',
|
||
created_by=self.request.user,
|
||
effective_date=timezone.now().date(),
|
||
expiry_date=template.expiry_date
|
||
)
|
||
|
||
# Log the creation
|
||
AuditLogEntry.objects.create(
|
||
tenant=self.request.user.tenant,
|
||
user=self.request.user,
|
||
action='CREATE',
|
||
entity_type='ConsentForm',
|
||
entity_id=str(consent_form.consent_id),
|
||
details={
|
||
'patient': patient.get_full_name(),
|
||
'template': template.name,
|
||
'status': 'PENDING'
|
||
}
|
||
)
|
||
|
||
messages.success(
|
||
self.request,
|
||
f"Consent form generated for {patient.get_full_name()}"
|
||
)
|
||
|
||
return redirect('patients:consent_form_detail', pk=consent_form.pk)
|
||
#
|
||
#
|
||
# class ConsentFormDetailView(LoginRequiredMixin, DetailView):
|
||
# """
|
||
# Display consent form details.
|
||
# """
|
||
# model = ConsentForm
|
||
# template_name = 'patients/consent_form_detail.html'
|
||
# context_object_name = 'consent_form'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return ConsentForm.objects.none()
|
||
# return ConsentForm.objects.filter(patient__tenant=tenant)
|
||
#
|
||
#
|
||
# class ConsentFormCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
# """
|
||
# Create new consent form.
|
||
# """
|
||
# model = ConsentForm
|
||
# form_class = ConsentFormForm
|
||
# template_name = 'patients/consent_form_form.html'
|
||
# permission_required = 'patients.add_consentform'
|
||
# success_url = reverse_lazy('patients:consent_form_list')
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# return kwargs
|
||
#
|
||
# def form_valid(self, form):
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log consent form creation
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(self.request, 'tenant', None),
|
||
# event_type='CREATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Create Consent Form',
|
||
# description=f'Created consent form: {self.object.template.name} for {self.object.patient}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request, f'Consent form "{self.object.template.name}" created successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # PATIENT NOTE VIEWS (APPEND-ONLY - Clinical Records)
|
||
# # ============================================================================
|
||
#
|
||
# class PatientNoteListView(LoginRequiredMixin, ListView):
|
||
# """
|
||
# List patient notes.
|
||
# """
|
||
# model = PatientNote
|
||
# template_name = 'patients/patient_note_list.html'
|
||
# context_object_name = 'patient_notes'
|
||
# paginate_by = 25
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return PatientNote.objects.none()
|
||
#
|
||
# queryset = PatientNote.objects.filter(patient__tenant=tenant)
|
||
#
|
||
# # Apply filters
|
||
# patient_id = self.request.GET.get('patient_id')
|
||
# if patient_id:
|
||
# queryset = queryset.filter(patient_id=patient_id)
|
||
#
|
||
# note_type = self.request.GET.get('note_type')
|
||
# if note_type:
|
||
# queryset = queryset.filter(note_type=note_type)
|
||
#
|
||
# is_confidential = self.request.GET.get('is_confidential')
|
||
# if is_confidential == 'true':
|
||
# queryset = queryset.filter(is_confidential=True)
|
||
# elif is_confidential == 'false':
|
||
# queryset = queryset.filter(is_confidential=False)
|
||
#
|
||
# search = self.request.GET.get('search')
|
||
# if search:
|
||
# queryset = queryset.filter(
|
||
# Q(subject__icontains=search) |
|
||
# Q(content__icontains=search)
|
||
# )
|
||
#
|
||
# return queryset.order_by('-created_at')
|
||
#
|
||
# def get_context_data(self, **kwargs):
|
||
# context = super().get_context_data(**kwargs)
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
#
|
||
# if tenant:
|
||
# context.update({
|
||
# 'patients': PatientProfile.objects.filter(tenant=tenant, is_active=True).order_by('last_name',
|
||
# 'first_name'),
|
||
# 'note_types': PatientNote.NOTE_TYPE_CHOICES,
|
||
# })
|
||
#
|
||
# return context
|
||
#
|
||
#
|
||
# class PatientNoteDetailView(LoginRequiredMixin, DetailView):
|
||
# """
|
||
# Display patient note details.
|
||
# """
|
||
# model = PatientNote
|
||
# template_name = 'patients/patient_note_detail.html'
|
||
# context_object_name = 'patient_note'
|
||
#
|
||
# def get_queryset(self):
|
||
# tenant = getattr(self.request, 'tenant', None)
|
||
# if not tenant:
|
||
# return PatientNote.objects.none()
|
||
# return PatientNote.objects.filter(patient__tenant=tenant)
|
||
#
|
||
#
|
||
# class PatientNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||
# """
|
||
# Create new patient note.
|
||
# """
|
||
# model = PatientNote
|
||
# form_class = PatientNoteForm
|
||
# template_name = 'patients/patient_note_form.html'
|
||
# permission_required = 'patients.add_patientnote'
|
||
# success_url = reverse_lazy('patients:patient_note_list')
|
||
#
|
||
# def get_form_kwargs(self):
|
||
# kwargs = super().get_form_kwargs()
|
||
# kwargs['user'] = self.request.user
|
||
# return kwargs
|
||
#
|
||
# def form_valid(self, form):
|
||
# # Set author
|
||
# form.instance.author = self.request.user
|
||
# response = super().form_valid(form)
|
||
#
|
||
# # Log patient note creation
|
||
# AuditLogger.log_event(
|
||
# tenant=getattr(self.request, 'tenant', None),
|
||
# event_type='CREATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Create Patient Note',
|
||
# description=f'Created patient note: {self.object.subject} for {self.object.patient}',
|
||
# user=self.request.user,
|
||
# content_object=self.object,
|
||
# request=self.request
|
||
# )
|
||
#
|
||
# messages.success(self.request, f'Patient note "{self.object.subject}" created successfully.')
|
||
# return response
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # HTMX VIEWS FOR REAL-TIME UPDATES
|
||
# # ============================================================================
|
||
#
|
||
# @login_required
|
||
# def patient_search(request):
|
||
# """
|
||
# HTMX view for patient search.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# search_query = request.GET.get('search', '')
|
||
# queryset = PatientProfile.objects.filter(tenant=tenant, is_active=True)
|
||
#
|
||
# if search_query:
|
||
# queryset = queryset.filter(
|
||
# Q(patient_id__icontains=search_query) |
|
||
# Q(first_name__icontains=search_query) |
|
||
# Q(last_name__icontains=search_query) |
|
||
# Q(email__icontains=search_query) |
|
||
# Q(phone_number__icontains=search_query)
|
||
# )
|
||
#
|
||
# patients = queryset.order_by('last_name', 'first_name')[:20]
|
||
#
|
||
# return render(request, 'patients/partials/patient_list.html', {
|
||
# 'patients': patients
|
||
# })
|
||
#
|
||
#
|
||
# @login_required
|
||
# def patient_stats(request):
|
||
# """
|
||
# HTMX view for patient statistics.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# # Calculate patient statistics
|
||
# total_patients = PatientProfile.objects.filter(tenant=tenant).count()
|
||
# active_patients = PatientProfile.objects.filter(tenant=tenant, is_active=True).count()
|
||
#
|
||
# # Age distribution
|
||
# today = timezone.now().date()
|
||
# pediatric_count = PatientProfile.objects.filter(
|
||
# tenant=tenant,
|
||
# is_active=True,
|
||
# date_of_birth__gt=today.replace(year=today.year - 18)
|
||
# ).count()
|
||
#
|
||
# adult_count = PatientProfile.objects.filter(
|
||
# tenant=tenant,
|
||
# is_active=True,
|
||
# date_of_birth__lte=today.replace(year=today.year - 18),
|
||
# date_of_birth__gt=today.replace(year=today.year - 65)
|
||
# ).count()
|
||
#
|
||
# geriatric_count = PatientProfile.objects.filter(
|
||
# tenant=tenant,
|
||
# is_active=True,
|
||
# date_of_birth__lte=today.replace(year=today.year - 65)
|
||
# ).count()
|
||
#
|
||
# # Gender distribution
|
||
# gender_stats = PatientProfile.objects.filter(tenant=tenant, is_active=True).values('gender').annotate(
|
||
# count=Count('id')
|
||
# ).order_by('-count')
|
||
#
|
||
# # Recent registrations (last 30 days)
|
||
# thirty_days_ago = today - timedelta(days=30)
|
||
# recent_registrations = PatientProfile.objects.filter(
|
||
# tenant=tenant,
|
||
# created_at__gte=thirty_days_ago
|
||
# ).count()
|
||
#
|
||
# stats = {
|
||
# 'total_patients': total_patients,
|
||
# 'active_patients': active_patients,
|
||
# 'pediatric_count': pediatric_count,
|
||
# 'adult_count': adult_count,
|
||
# 'geriatric_count': geriatric_count,
|
||
# 'gender_stats': gender_stats,
|
||
# 'recent_registrations': recent_registrations,
|
||
# }
|
||
#
|
||
# return render(request, 'patients/partials/patient_stats.html', {'stats': stats})
|
||
#
|
||
#
|
||
# @login_required
|
||
# def emergency_contacts_list(request, patient_id):
|
||
# """
|
||
# HTMX view for emergency contacts list.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
# emergency_contacts = EmergencyContact.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_primary', 'name')
|
||
#
|
||
# return render(request, 'patients/partials/emergency_contacts_list.html', {
|
||
# 'emergency_contacts': emergency_contacts,
|
||
# 'patient': patient
|
||
# })
|
||
#
|
||
|
||
@login_required
|
||
def insurance_details_api(request, insurance_id):
|
||
insurance_info = get_object_or_404(InsuranceInfo, pk=insurance_id)
|
||
return JsonResponse({'insurance_info': insurance_info})
|
||
|
||
|
||
|
||
# @login_required
|
||
# def insurance_info_list(request, patient_id):
|
||
# """
|
||
# HTMX view for insurance info list.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
# insurance_info = InsuranceInfo.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_primary', '-effective_date')
|
||
#
|
||
# return render(request, 'patients/partials/insurance_info_list.html', {
|
||
# 'insurance_info': insurance_info,
|
||
# 'patient': patient
|
||
# })
|
||
#
|
||
#
|
||
# @login_required
|
||
# def consent_forms_list(request, patient_id):
|
||
# """
|
||
# HTMX view for consent forms list.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
# consent_forms = ConsentForm.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-created_at')
|
||
#
|
||
# return render(request, 'patients/partials/consent_forms_list.html', {
|
||
# 'consent_forms': consent_forms,
|
||
# 'patient': patient
|
||
# })
|
||
#
|
||
#
|
||
# @login_required
|
||
# def patient_notes_list(request, patient_id):
|
||
# """
|
||
# HTMX view for patient notes list.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
# patient_notes = PatientNote.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-created_at')[:10]
|
||
#
|
||
# return render(request, 'patients/partials/patient_notes_list.html', {
|
||
# 'patient_notes': patient_notes,
|
||
# 'patient': patient
|
||
# })
|
||
#
|
||
#
|
||
# # ============================================================================
|
||
# # ACTION VIEWS FOR WORKFLOW OPERATIONS
|
||
# # ============================================================================
|
||
#
|
||
# @login_required
|
||
# def activate_patient(request, pk):
|
||
# """
|
||
# Activate a patient account.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# messages.error(request, 'No tenant found.')
|
||
# return redirect('patients:patient_list')
|
||
#
|
||
# patient = get_object_or_404(PatientProfile, pk=pk, tenant=tenant)
|
||
#
|
||
# patient.is_active = True
|
||
# patient.save()
|
||
#
|
||
# # Log activation
|
||
# AuditLogger.log_event(
|
||
# tenant=tenant,
|
||
# event_type='UPDATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Activate Patient',
|
||
# description=f'Activated patient: {patient.first_name} {patient.last_name}',
|
||
# user=request.user,
|
||
# content_object=patient,
|
||
# request=request
|
||
# )
|
||
#
|
||
# messages.success(request, f'Patient "{patient.first_name} {patient.last_name}" activated successfully.')
|
||
# return redirect('patients:patient_detail', pk=pk)
|
||
#
|
||
#
|
||
# @login_required
|
||
# def sign_consent_form(request, pk):
|
||
# """
|
||
# Sign a consent form.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# if not tenant:
|
||
# return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
#
|
||
# consent_form = get_object_or_404(
|
||
# ConsentForm,
|
||
# pk=pk,
|
||
# patient__tenant=tenant
|
||
# )
|
||
#
|
||
# if request.method == 'POST':
|
||
# # Update consent form status
|
||
# consent_form.status = 'SIGNED'
|
||
# consent_form.signed_date = timezone.now()
|
||
# consent_form.signed_by = request.user
|
||
#
|
||
# # Add witness information if required
|
||
# if consent_form.template.requires_witness:
|
||
# consent_form.witness_name = request.POST.get('witness_name')
|
||
# consent_form.witness_signature = request.POST.get('witness_signature')
|
||
#
|
||
# consent_form.save()
|
||
#
|
||
# # Log consent form signing
|
||
# AuditLogger.log_event(
|
||
# tenant=tenant,
|
||
# event_type='UPDATE',
|
||
# event_category='PATIENT_MANAGEMENT',
|
||
# action='Sign Consent Form',
|
||
# description=f'Signed consent form: {consent_form.template.name} for {consent_form.patient}',
|
||
# user=request.user,
|
||
# content_object=consent_form,
|
||
# request=request
|
||
# )
|
||
#
|
||
# messages.success(request, f'Consent form "{consent_form.template.name}" signed successfully.')
|
||
# return JsonResponse({'status': 'signed'})
|
||
#
|
||
# return render(request, 'patients/partials/sign_consent_form.html', {
|
||
# 'consent_form': consent_form
|
||
# })
|
||
#
|
||
#
|
||
# @login_required
|
||
# def add_patient_note(request, patient_id):
|
||
# """
|
||
# HTMX view for adding a patient note.
|
||
# """
|
||
# patient = get_object_or_404(
|
||
# PatientProfile,
|
||
# pk=patient_id,
|
||
# tenant=getattr(request, 'tenant', None)
|
||
# )
|
||
#
|
||
# if request.method == 'POST':
|
||
# form = PatientNoteForm(request.POST, user=request.user)
|
||
# form.user = request.user # Set user for form.save() method
|
||
#
|
||
# if form.is_valid():
|
||
# note = form.save(commit=False)
|
||
# note.patient = patient
|
||
# note.created_by = request.user
|
||
# note.save()
|
||
#
|
||
# # Fetch the latest notes to refresh the list
|
||
# notes = PatientNote.objects.filter(
|
||
# patient=patient,
|
||
# is_active=True
|
||
# ).order_by('-created_at')
|
||
#
|
||
# return render(request, 'patients/partials/patient_notes_list.html', {
|
||
# 'notes': notes,
|
||
# 'patient': patient
|
||
# })
|
||
# else:
|
||
# # Return form with errors
|
||
# return render(request, 'patients/partials/add_patient_note_form.html', {
|
||
# 'form': form,
|
||
# 'patient': patient
|
||
# })
|
||
# else:
|
||
# # Initialize the form with patient pre-selected
|
||
# initial_data = {'patient': patient.pk}
|
||
# form = PatientNoteForm(initial=initial_data, user=request.user)
|
||
#
|
||
# return render(request, 'patients/partials/add_patient_note_form.html', {
|
||
# 'form': form,
|
||
# 'patient': patient
|
||
# })
|
||
|
||
|
||
@login_required
|
||
def patient_demographics_update(request, patient_id):
|
||
"""
|
||
HTMX view for updating patient demographics.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
patient = get_object_or_404(PatientProfile, pk=patient_id, tenant=tenant)
|
||
|
||
if request.method == 'POST':
|
||
form = PatientProfileForm(request.POST, instance=patient)
|
||
if form.is_valid():
|
||
form.save()
|
||
|
||
# Log the update
|
||
from core.models import AuditLogEntry
|
||
AuditLogEntry.objects.create(
|
||
tenant=tenant,
|
||
user=request.user,
|
||
action='UPDATE',
|
||
entity_type='PatientProfile',
|
||
entity_id=str(patient.patient_id),
|
||
details={
|
||
'name': patient.get_full_name(),
|
||
'mrn': patient.mrn,
|
||
'fields_updated': list(form.changed_data)
|
||
}
|
||
)
|
||
|
||
messages.success(request, 'Patient demographics updated successfully.')
|
||
return JsonResponse({'status': 'success'})
|
||
else:
|
||
return JsonResponse({'status': 'error', 'errors': form.errors})
|
||
else:
|
||
form = PatientProfileForm(instance=patient)
|
||
|
||
return render(request, 'patients/partials/demographics_form.html', {
|
||
'form': form,
|
||
'patient': patient
|
||
})
|
||
|
||
|
||
@login_required
|
||
def verify_insurance(request, insurance_id):
|
||
"""
|
||
HTMX view for verifying insurance information.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
insurance = get_object_or_404(InsuranceInfo, pk=insurance_id, patient__tenant=tenant)
|
||
|
||
if request.method == 'POST':
|
||
insurance.is_verified = True
|
||
insurance.verification_date = timezone.now()
|
||
insurance.verified_by = request.user
|
||
insurance.save()
|
||
|
||
# Log the verification
|
||
from core.models import AuditLogEntry
|
||
AuditLogEntry.objects.create(
|
||
tenant=tenant,
|
||
user=request.user,
|
||
action='UPDATE',
|
||
entity_type='InsuranceInfo',
|
||
entity_id=str(insurance.id),
|
||
details={
|
||
'patient': insurance.patient.get_full_name(),
|
||
'mrn': insurance.patient.mrn,
|
||
'insurance': insurance.insurance_company,
|
||
'policy': insurance.policy_number,
|
||
'verification_date': str(insurance.verification_date)
|
||
}
|
||
)
|
||
|
||
messages.success(request, 'Insurance information verified successfully.')
|
||
|
||
# Fetch all insurance records for this patient to refresh the list
|
||
insurance_records = InsuranceInfo.objects.filter(
|
||
patient=insurance.patient
|
||
).order_by('-is_active', '-effective_date')
|
||
|
||
return render(request, 'patients/partials/insurance_info_list.html', {
|
||
'insurance_records': insurance_records,
|
||
'patient': insurance.patient
|
||
})
|
||
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
|
||
|
||
|
||
# @login_required
|
||
# def emergency_contacts_list(request, patient_id):
|
||
# """
|
||
# HTMX view for listing emergency contacts.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# patient = get_object_or_404(PatientProfile, pk=patient_id, tenant=tenant)
|
||
#
|
||
# contacts = EmergencyContact.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_active', 'priority')
|
||
#
|
||
# # Handle form submission for adding a new contact
|
||
# if request.method == 'POST':
|
||
# form = EmergencyContactForm(request.POST, user=request.user)
|
||
# if form.is_valid():
|
||
# contact = form.save(commit=False)
|
||
# contact.patient = patient
|
||
# contact.save()
|
||
#
|
||
# # Log the addition
|
||
# from core.models import AuditLogEntry
|
||
# AuditLogEntry.objects.create(
|
||
# tenant=tenant,
|
||
# user=request.user,
|
||
# action='CREATE',
|
||
# entity_type='EmergencyContact',
|
||
# entity_id=str(contact.id),
|
||
# details={
|
||
# 'patient': patient.get_full_name(),
|
||
# 'mrn': patient.mrn,
|
||
# 'contact': contact.get_full_name(),
|
||
# 'relationship': contact.relationship
|
||
# }
|
||
# )
|
||
#
|
||
# messages.success(request, 'Emergency contact added successfully.')
|
||
#
|
||
# # Refresh contacts list
|
||
# contacts = EmergencyContact.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_active', 'priority')
|
||
#
|
||
# return render(request, 'patients/partials/emergency_contacts_list.html', {
|
||
# 'contacts': contacts,
|
||
# 'patient': patient
|
||
# })
|
||
# else:
|
||
# # Return form with errors
|
||
# return render(request, 'patients/emergency_contacts/emergency_contact_form.html', {
|
||
# 'form': form,
|
||
# 'patient': patient
|
||
# })
|
||
# else:
|
||
# # Initial form for adding new contact
|
||
# initial_data = {'patient': patient.pk}
|
||
# form = EmergencyContactForm(initial=initial_data, user=request.user)
|
||
#
|
||
# return render(request, 'patients/partials/emergency_contacts_list.html', {
|
||
# 'contacts': contacts,
|
||
# 'patient': patient,
|
||
# 'form': form
|
||
# })
|
||
#
|
||
#
|
||
# @login_required
|
||
# def insurance_info_list(request, patient_id):
|
||
# """
|
||
# HTMX view for listing insurance information.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# patient = get_object_or_404(PatientProfile, pk=patient_id, tenant=tenant)
|
||
#
|
||
# insurance_records = InsuranceInfo.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_active', '-effective_date')
|
||
#
|
||
# # Handle form submission for adding new insurance
|
||
# if request.method == 'POST':
|
||
# form = InsuranceInfoForm(request.POST, user=request.user)
|
||
# if form.is_valid():
|
||
# insurance = form.save(commit=False)
|
||
# insurance.patient = patient
|
||
# insurance.save()
|
||
#
|
||
# # Log the addition
|
||
# from core.models import AuditLogEntry
|
||
# AuditLogEntry.objects.create(
|
||
# tenant=tenant,
|
||
# user=request.user,
|
||
# action='CREATE',
|
||
# entity_type='InsuranceInfo',
|
||
# entity_id=str(insurance.id),
|
||
# details={
|
||
# 'patient': patient.get_full_name(),
|
||
# 'mrn': patient.mrn,
|
||
# 'insurance': insurance.insurance_company,
|
||
# 'policy': insurance.policy_number
|
||
# }
|
||
# )
|
||
#
|
||
# messages.success(request, 'Insurance information added successfully.')
|
||
#
|
||
# # Refresh insurance list
|
||
# insurance_records = InsuranceInfo.objects.filter(
|
||
# patient=patient
|
||
# ).order_by('-is_active', '-effective_date')
|
||
#
|
||
# return render(request, 'patients/partials/insurance_info_list.html', {
|
||
# 'insurance_records': insurance_records,
|
||
# 'patient': patient
|
||
# })
|
||
# else:
|
||
# # Return form with errors
|
||
# return render(request, 'patients/partials/insurance_info_form.html', {
|
||
# 'form': form,
|
||
# 'patient': patient
|
||
# })
|
||
# else:
|
||
# # Initial form for adding new insurance
|
||
# initial_data = {'patient': patient.pk}
|
||
# form = InsuranceInfoForm(initial=initial_data, user=request.user)
|
||
#
|
||
# return render(request, 'patients/partials/insurance_info_list.html', {
|
||
# 'insurance_records': insurance_records,
|
||
# 'patient': patient,
|
||
# 'form': form
|
||
# })
|
||
#
|
||
#
|
||
# @login_required
|
||
# def patient_notes_list(request, patient_id):
|
||
# """
|
||
# HTMX view for listing patient notes.
|
||
# """
|
||
# tenant = getattr(request, 'tenant', None)
|
||
# patient = get_object_or_404(PatientProfile, pk=patient_id, tenant=tenant)
|
||
#
|
||
# notes = PatientNote.objects.filter(
|
||
# patient=patient,
|
||
# is_active=True
|
||
# ).order_by('-created_at')
|
||
#
|
||
# return render(request, 'patients/partials/patient_notes_list.html', {
|
||
# 'notes': notes,
|
||
# 'patient': patient
|
||
# })
|
||
#
|
||
#
|
||
#
|
||
|
||
@login_required
|
||
def check_eligibility(request, pk):
|
||
"""
|
||
AJAX view to check insurance eligibility for a patient.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
# Get insurance info object
|
||
from .models import InsuranceInfo
|
||
insurance = get_object_or_404(InsuranceInfo, pk=pk, patient__tenant=tenant)
|
||
|
||
if request.method == 'POST':
|
||
# Simulate eligibility check (in real implementation, this would call insurance API)
|
||
import random
|
||
from datetime import datetime, timedelta
|
||
|
||
# Simulate API call delay
|
||
import time
|
||
time.sleep(1)
|
||
|
||
# Generate mock eligibility data
|
||
eligibility_status = random.choice(['active', 'inactive', 'pending', 'expired'])
|
||
coverage_types = ['medical', 'dental', 'vision', 'prescription']
|
||
active_coverage = random.sample(coverage_types, random.randint(1, 4))
|
||
|
||
# Calculate mock deductible and copay amounts
|
||
deductible_remaining = random.randint(0, 5000)
|
||
copay_amount = random.choice([10, 15, 20, 25, 30, 35, 40, 50])
|
||
|
||
# Generate mock effective dates
|
||
effective_date = datetime.now() - timedelta(days=random.randint(30, 365))
|
||
expiration_date = effective_date + timedelta(days=365)
|
||
|
||
eligibility_data = {
|
||
'status': eligibility_status,
|
||
'effective_date': effective_date.strftime('%Y-%m-%d'),
|
||
'expiration_date': expiration_date.strftime('%Y-%m-%d'),
|
||
'coverage_types': active_coverage,
|
||
'deductible_remaining': deductible_remaining,
|
||
'copay_amount': copay_amount,
|
||
'last_checked': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||
'verification_id': f'VER{random.randint(100000, 999999)}',
|
||
'group_number': insurance.group_number or f'GRP{random.randint(10000, 99999)}',
|
||
'prior_authorization_required': random.choice([True, False]),
|
||
'network_status': random.choice(['in-network', 'out-of-network', 'preferred']),
|
||
}
|
||
|
||
# Log eligibility check
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='READ',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Check Insurance Eligibility',
|
||
description=f'Checked eligibility for {insurance.patient} - {insurance.insurance_company}',
|
||
user=request.user,
|
||
content_object=insurance,
|
||
request=request
|
||
)
|
||
|
||
# Render the eligibility results template
|
||
html = render(request, 'patients/partials/eligibility_results.html', {
|
||
'insurance': insurance,
|
||
'eligibility': eligibility_data
|
||
}).content.decode('utf-8')
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'html': html,
|
||
'eligibility_status': eligibility_status
|
||
})
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def renew_insurance(request, pk):
|
||
"""
|
||
AJAX view to renew/mark insurance for renewal.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
# Get insurance info object
|
||
from .models import InsuranceInfo
|
||
insurance = get_object_or_404(InsuranceInfo, id=pk, patient__tenant=tenant)
|
||
|
||
if request.method == 'POST':
|
||
try:
|
||
# Update insurance renewal status
|
||
insurance.renewal_requested = True
|
||
insurance.renewal_requested_date = timezone.now()
|
||
insurance.renewal_requested_by = request.user
|
||
|
||
# If expired, extend expiration date by 1 year
|
||
if insurance.expiration_date and insurance.is_expired:
|
||
from datetime import timedelta
|
||
insurance.expiration_date = insurance.expiration_date + timedelta(days=365)
|
||
elif not insurance.expiration_date:
|
||
# Set expiration date to 1 year from now if not set
|
||
from datetime import timedelta
|
||
insurance.expiration_date = timezone.now().date() + timedelta(days=365)
|
||
|
||
# Update status if needed
|
||
if hasattr(insurance, 'status'):
|
||
insurance.status = 'renewal_pending'
|
||
|
||
insurance.save()
|
||
|
||
# Log insurance renewal request
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Request Insurance Renewal',
|
||
description=f'Requested renewal for {insurance.patient} - {insurance.insurance_company}',
|
||
user=request.user,
|
||
content_object=insurance,
|
||
request=request
|
||
)
|
||
|
||
# Create notification for insurance team (if notification system exists)
|
||
try:
|
||
from notifications.models import Notification
|
||
Notification.objects.create(
|
||
tenant=tenant,
|
||
title='Insurance Renewal Request',
|
||
message=f'Renewal requested for {insurance.patient.get_full_name()} - {insurance.insurance_company}',
|
||
notification_type='insurance_renewal',
|
||
created_by=request.user,
|
||
target_users=['insurance_team'], # This would be configured based on your notification system
|
||
)
|
||
except ImportError:
|
||
# Notification system not available
|
||
pass
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'message': 'Insurance marked for renewal successfully',
|
||
'renewal_date': insurance.renewal_requested_date.strftime(
|
||
'%Y-%m-%d %H:%M:%S') if insurance.renewal_requested_date else None,
|
||
'expiration_date': insurance.expiration_date.strftime('%Y-%m-%d') if insurance.expiration_date else None
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': f'Failed to process renewal request: {str(e)}'
|
||
}, status=500)
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def bulk_renew_insurance(request):
|
||
"""
|
||
AJAX view to bulk renew multiple insurance records.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
if request.method == 'POST':
|
||
insurance_ids = request.POST.getlist('insurance_ids[]')
|
||
|
||
if not insurance_ids:
|
||
return JsonResponse({'error': 'No insurance records selected'}, status=400)
|
||
|
||
try:
|
||
from .models import InsuranceInfo
|
||
from datetime import timedelta
|
||
|
||
updated_count = 0
|
||
errors = []
|
||
|
||
for insurance_id in insurance_ids:
|
||
try:
|
||
insurance = InsuranceInfo.objects.get(
|
||
id=insurance_id,
|
||
patient__tenant=tenant
|
||
)
|
||
|
||
# Update insurance renewal status
|
||
insurance.renewal_requested = True
|
||
insurance.renewal_requested_date = timezone.now()
|
||
insurance.renewal_requested_by = request.user
|
||
|
||
# If expired, extend expiration date by 1 year
|
||
if insurance.expiration_date and insurance.is_expired:
|
||
insurance.expiration_date = insurance.expiration_date + timedelta(days=365)
|
||
elif not insurance.expiration_date:
|
||
# Set expiration date to 1 year from now if not set
|
||
insurance.expiration_date = timezone.now().date() + timedelta(days=365)
|
||
|
||
# Update status if needed
|
||
if hasattr(insurance, 'status'):
|
||
insurance.status = 'renewal_pending'
|
||
|
||
insurance.save()
|
||
updated_count += 1
|
||
|
||
# Log individual renewal request
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Bulk Request Insurance Renewal',
|
||
description=f'Bulk renewal requested for {insurance.patient} - {insurance.insurance_company}',
|
||
user=request.user,
|
||
content_object=insurance,
|
||
request=request
|
||
)
|
||
|
||
except InsuranceInfo.DoesNotExist:
|
||
errors.append(f'Insurance record {insurance_id} not found')
|
||
except Exception as e:
|
||
errors.append(f'Error processing insurance {insurance_id}: {str(e)}')
|
||
|
||
# Create bulk notification for insurance team
|
||
try:
|
||
from notifications.models import Notification
|
||
Notification.objects.create(
|
||
tenant=tenant,
|
||
title='Bulk Insurance Renewal Request',
|
||
message=f'Bulk renewal requested for {updated_count} insurance records',
|
||
notification_type='bulk_insurance_renewal',
|
||
created_by=request.user,
|
||
target_users=['insurance_team'],
|
||
)
|
||
except ImportError:
|
||
# Notification system not available
|
||
pass
|
||
|
||
response_data = {
|
||
'status': 'success',
|
||
'updated_count': updated_count,
|
||
'message': f'Successfully marked {updated_count} insurance records for renewal'
|
||
}
|
||
|
||
if errors:
|
||
response_data['errors'] = errors
|
||
response_data['message'] += f' ({len(errors)} errors occurred)'
|
||
|
||
return JsonResponse(response_data)
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': f'Failed to process bulk renewal: {str(e)}'
|
||
}, status=500)
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def insurance_claims_history(request, pk):
|
||
"""
|
||
AJAX view to load insurance claims history for a patient's insurance.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
# Get insurance info object
|
||
from .models import InsuranceInfo
|
||
insurance = get_object_or_404(InsuranceInfo, id=pk, patient__tenant=tenant)
|
||
|
||
if request.method == 'GET':
|
||
# Generate mock claims history (in real implementation, this would query claims database)
|
||
import random
|
||
from datetime import datetime, timedelta
|
||
|
||
# Generate mock claims data
|
||
claims = []
|
||
claim_types = [
|
||
'Office Visit', 'Emergency Room', 'Prescription', 'Laboratory',
|
||
'Radiology', 'Surgery', 'Physical Therapy', 'Specialist Consultation',
|
||
'Dental', 'Vision', 'Mental Health', 'Preventive Care'
|
||
]
|
||
|
||
claim_statuses = ['approved', 'pending', 'denied', 'processing', 'paid']
|
||
|
||
# Generate 10-20 mock claims
|
||
num_claims = random.randint(10, 20)
|
||
|
||
for i in range(num_claims):
|
||
claim_date = datetime.now() - timedelta(days=random.randint(1, 730)) # Last 2 years
|
||
service_date = claim_date - timedelta(days=random.randint(0, 30))
|
||
|
||
claim_amount = random.uniform(50, 5000)
|
||
approved_amount = claim_amount * random.uniform(0.7, 1.0)
|
||
patient_responsibility = claim_amount - approved_amount
|
||
|
||
status = random.choice(claim_statuses)
|
||
|
||
claim = {
|
||
'claim_id': f'CLM{random.randint(100000, 999999)}',
|
||
'service_date': service_date.strftime('%Y-%m-%d'),
|
||
'claim_date': claim_date.strftime('%Y-%m-%d'),
|
||
'service_type': random.choice(claim_types),
|
||
'provider': f'Dr. {random.choice(["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"])}',
|
||
'claim_amount': round(claim_amount, 2),
|
||
'approved_amount': round(approved_amount, 2) if status in ['approved', 'paid'] else 0,
|
||
'patient_responsibility': round(patient_responsibility, 2) if status in ['approved', 'paid'] else 0,
|
||
'status': status,
|
||
'diagnosis_code': f'{random.choice(["Z00", "M79", "R06", "K59", "I10", "E11", "F32", "G43", "J06", "N39"])}.{random.randint(10, 99)}',
|
||
'procedure_code': f'{random.randint(99000, 99999)}',
|
||
'processed_date': (claim_date + timedelta(days=random.randint(1, 14))).strftime(
|
||
'%Y-%m-%d') if status != 'pending' else None,
|
||
'payment_date': (claim_date + timedelta(days=random.randint(15, 45))).strftime(
|
||
'%Y-%m-%d') if status == 'paid' else None,
|
||
'denial_reason': random.choice([
|
||
'Service not covered', 'Prior authorization required', 'Duplicate claim',
|
||
'Information incomplete', 'Out of network provider'
|
||
]) if status == 'denied' else None,
|
||
}
|
||
claims.append(claim)
|
||
|
||
# Sort claims by service date (most recent first)
|
||
claims.sort(key=lambda x: x['service_date'], reverse=True)
|
||
|
||
# Calculate summary statistics
|
||
total_claims = len(claims)
|
||
total_claimed = sum(claim['claim_amount'] for claim in claims)
|
||
total_approved = sum(claim['approved_amount'] for claim in claims)
|
||
total_patient_responsibility = sum(claim['patient_responsibility'] for claim in claims)
|
||
|
||
approved_claims = len([c for c in claims if c['status'] in ['approved', 'paid']])
|
||
pending_claims = len([c for c in claims if c['status'] == 'pending'])
|
||
denied_claims = len([c for c in claims if c['status'] == 'denied'])
|
||
|
||
summary = {
|
||
'total_claims': total_claims,
|
||
'total_claimed': round(total_claimed, 2),
|
||
'total_approved': round(total_approved, 2),
|
||
'total_patient_responsibility': round(total_patient_responsibility, 2),
|
||
'approved_claims': approved_claims,
|
||
'pending_claims': pending_claims,
|
||
'denied_claims': denied_claims,
|
||
'approval_rate': round((approved_claims / total_claims * 100), 1) if total_claims > 0 else 0,
|
||
}
|
||
|
||
# Log claims history access
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='READ',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='View Insurance Claims History',
|
||
description=f'Viewed claims history for {insurance.patient} - {insurance.insurance_company}',
|
||
user=request.user,
|
||
content_object=insurance,
|
||
request=request
|
||
)
|
||
|
||
# Render the claims history template
|
||
html = render(request, 'patients/partials/insurance_claims_history.html', {
|
||
'insurance': insurance,
|
||
'claims': claims,
|
||
'summary': summary
|
||
}).content.decode('utf-8')
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'html': html,
|
||
'summary': summary
|
||
})
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def check_primary_insurance(request):
|
||
"""
|
||
AJAX view to check if a patient already has primary insurance.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
if request.method == 'POST':
|
||
patient_id = request.POST.get('patient_id')
|
||
current_insurance_id = request.POST.get('current_insurance_id')
|
||
|
||
if not patient_id:
|
||
return JsonResponse({'error': 'Patient ID is required'}, status=400)
|
||
|
||
try:
|
||
from .models import InsuranceInfo, PatientProfile
|
||
|
||
# Get patient
|
||
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
|
||
# Check for existing primary insurance
|
||
primary_insurance_query = InsuranceInfo.objects.filter(
|
||
patient=patient,
|
||
is_primary=True
|
||
)
|
||
|
||
# Exclude current insurance if editing
|
||
if current_insurance_id and current_insurance_id != 'null':
|
||
primary_insurance_query = primary_insurance_query.exclude(id=current_insurance_id)
|
||
|
||
existing_primary = primary_insurance_query.first()
|
||
|
||
response_data = {
|
||
'has_primary': existing_primary is not None,
|
||
'patient_name': patient.get_full_name(),
|
||
'patient_id': patient.patient_id or patient.id,
|
||
}
|
||
|
||
if existing_primary:
|
||
response_data.update({
|
||
'existing_primary': {
|
||
'id': existing_primary.id,
|
||
'provider': existing_primary.insurance_company,
|
||
'policy_number': existing_primary.policy_number,
|
||
'member_id': existing_primary.policy_number,
|
||
'effective_date': existing_primary.effective_date.strftime(
|
||
'%Y-%m-%d') if existing_primary.effective_date else None,
|
||
'expiration_date': existing_primary.termination_date.strftime(
|
||
'%Y-%m-%d') if existing_primary.termination_date else None,
|
||
'is_active': not existing_primary.is_expired if hasattr(existing_primary,
|
||
'is_expired') else True,
|
||
}
|
||
})
|
||
|
||
# Get all insurance records for this patient for additional context
|
||
all_insurance = InsuranceInfo.objects.filter(patient=patient).order_by('-is_primary', '-created_at')
|
||
insurance_list = []
|
||
|
||
for insurance in all_insurance:
|
||
insurance_info = {
|
||
'id': insurance.id,
|
||
'provider': insurance.insurance_company,
|
||
'policy_number': insurance.policy_number,
|
||
'member_id': insurance.policy_number,
|
||
'is_primary': insurance.is_primary,
|
||
'effective_date': insurance.effective_date.strftime(
|
||
'%Y-%m-%d') if insurance.effective_date else None,
|
||
'expiration_date': insurance.termination_date.strftime(
|
||
'%Y-%m-%d') if insurance.termination_date else None,
|
||
'is_active': not insurance.is_expired if hasattr(insurance, 'is_expired') else True,
|
||
'created_at': insurance.created_at.strftime('%Y-%m-%d') if insurance.created_at else None,
|
||
}
|
||
insurance_list.append(insurance_info)
|
||
|
||
response_data['all_insurance'] = insurance_list
|
||
response_data['total_insurance_count'] = len(insurance_list)
|
||
|
||
# Log primary insurance check
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='READ',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Check Primary Insurance',
|
||
description=f'Checked primary insurance status for {patient}',
|
||
user=request.user,
|
||
content_object=patient,
|
||
request=request
|
||
)
|
||
|
||
return JsonResponse(response_data)
|
||
|
||
except PatientProfile.DoesNotExist:
|
||
return JsonResponse({'error': 'Patient not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'error': f'Error checking primary insurance: {str(e)}'}, status=500)
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def validate_policy_number(request):
|
||
"""
|
||
AJAX view to validate insurance policy number uniqueness and format.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
if request.method == 'POST':
|
||
policy_number = request.POST.get('policy_number', '').strip()
|
||
provider = request.POST.get('provider', '').strip()
|
||
current_insurance_id = request.POST.get('current_insurance_id')
|
||
|
||
if not policy_number:
|
||
return JsonResponse({
|
||
'is_valid': False,
|
||
'error': 'Policy number is required'
|
||
})
|
||
|
||
if not provider:
|
||
return JsonResponse({
|
||
'is_valid': False,
|
||
'error': 'Insurance provider is required'
|
||
})
|
||
|
||
try:
|
||
from .models import InsuranceInfo
|
||
import re
|
||
|
||
# Basic policy number format validation
|
||
validation_result = {
|
||
'is_valid': True,
|
||
'warnings': [],
|
||
'suggestions': [],
|
||
'format_valid': True,
|
||
'uniqueness_valid': True,
|
||
'provider_specific_validation': True
|
||
}
|
||
|
||
# Format validation based on common insurance policy number patterns
|
||
format_patterns = {
|
||
'Aetna': r'^[A-Z]{2}\d{8,12}$',
|
||
'Blue Cross Blue Shield': r'^[A-Z]{3}\d{9}$',
|
||
'Cigna': r'^[A-Z0-9]{8,15}$',
|
||
'UnitedHealthcare': r'^[A-Z0-9]{9,12}$',
|
||
'Humana': r'^[A-Z0-9]{8,14}$',
|
||
'Kaiser Permanente': r'^[A-Z0-9]{7,12}$',
|
||
'Anthem': r'^[A-Z]{2,3}\d{8,10}$',
|
||
'Medicare': r'^[A-Z0-9]{11}$',
|
||
'Medicaid': r'^[A-Z0-9]{8,15}$',
|
||
}
|
||
|
||
# Check format for known providers
|
||
provider_pattern = None
|
||
for known_provider, pattern in format_patterns.items():
|
||
if known_provider.lower() in provider.lower():
|
||
provider_pattern = pattern
|
||
break
|
||
|
||
if provider_pattern:
|
||
if not re.match(provider_pattern, policy_number.upper()):
|
||
validation_result['format_valid'] = False
|
||
validation_result['warnings'].append(f'Policy number format may not match {provider} standards')
|
||
validation_result['suggestions'].append(f'Expected format for {provider}: {provider_pattern}')
|
||
else:
|
||
# Generic validation for unknown providers
|
||
if len(policy_number) < 6:
|
||
validation_result['format_valid'] = False
|
||
validation_result['warnings'].append('Policy number seems too short')
|
||
elif len(policy_number) > 20:
|
||
validation_result['format_valid'] = False
|
||
validation_result['warnings'].append('Policy number seems too long')
|
||
|
||
# Check for common invalid characters
|
||
if re.search(r'[^A-Za-z0-9\-]', policy_number):
|
||
validation_result['format_valid'] = False
|
||
validation_result['warnings'].append('Policy number contains invalid characters')
|
||
|
||
# Check uniqueness within the tenant
|
||
existing_query = InsuranceInfo.objects.filter(
|
||
patient__tenant=tenant,
|
||
policy_number__iexact=policy_number,
|
||
insurance_provider__iexact=provider
|
||
)
|
||
|
||
# Exclude current insurance if editing
|
||
if current_insurance_id and current_insurance_id != 'null':
|
||
existing_query = existing_query.exclude(id=current_insurance_id)
|
||
|
||
existing_insurance = existing_query.first()
|
||
|
||
if existing_insurance:
|
||
validation_result['uniqueness_valid'] = False
|
||
validation_result['is_valid'] = False
|
||
validation_result['warnings'].append('Policy number already exists for this provider')
|
||
validation_result['existing_insurance'] = {
|
||
'id': existing_insurance.id,
|
||
'patient_name': existing_insurance.patient.get_full_name(),
|
||
'patient_id': existing_insurance.patient.patient_id or existing_insurance.patient.id,
|
||
'effective_date': existing_insurance.effective_date.strftime(
|
||
'%Y-%m-%d') if existing_insurance.effective_date else None,
|
||
'is_primary': existing_insurance.is_primary,
|
||
}
|
||
|
||
# Check for similar policy numbers (potential typos)
|
||
similar_policies = InsuranceInfo.objects.filter(
|
||
patient__tenant=tenant,
|
||
insurance_provider__iexact=provider,
|
||
policy_number__icontains=policy_number[:6] # Check first 6 characters
|
||
)
|
||
|
||
if current_insurance_id and current_insurance_id != 'null':
|
||
similar_policies = similar_policies.exclude(id=current_insurance_id)
|
||
|
||
if similar_policies.exists() and not existing_insurance:
|
||
similar_list = []
|
||
for similar in similar_policies[:3]: # Limit to 3 suggestions
|
||
similar_list.append({
|
||
'policy_number': similar.policy_number,
|
||
'patient_name': similar.patient.get_full_name(),
|
||
'patient_id': similar.patient.patient_id or similar.patient.id,
|
||
})
|
||
|
||
if similar_list:
|
||
validation_result['suggestions'].append('Similar policy numbers found')
|
||
validation_result['similar_policies'] = similar_list
|
||
|
||
# Provider-specific validation rules
|
||
provider_lower = provider.lower()
|
||
|
||
if 'medicare' in provider_lower:
|
||
# Medicare-specific validation
|
||
if not re.match(r'^[A-Z0-9]{11}$', policy_number.upper()):
|
||
validation_result['provider_specific_validation'] = False
|
||
validation_result['warnings'].append(
|
||
'Medicare policy numbers should be 11 characters (letters and numbers)')
|
||
|
||
elif 'medicaid' in provider_lower:
|
||
# Medicaid-specific validation
|
||
if len(policy_number) < 8:
|
||
validation_result['provider_specific_validation'] = False
|
||
validation_result['warnings'].append('Medicaid policy numbers are typically 8+ characters')
|
||
|
||
# Overall validation result
|
||
if not validation_result['format_valid'] or not validation_result['uniqueness_valid']:
|
||
validation_result['is_valid'] = False
|
||
elif validation_result['warnings']:
|
||
# Valid but with warnings
|
||
validation_result['is_valid'] = True
|
||
validation_result['has_warnings'] = True
|
||
|
||
# Log policy number validation
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='VALIDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Validate Policy Number',
|
||
description=f'Validated policy number {policy_number} for provider {provider}',
|
||
user=request.user,
|
||
request=request
|
||
)
|
||
|
||
return JsonResponse(validation_result)
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'is_valid': False,
|
||
'error': f'Error validating policy number: {str(e)}'
|
||
}, status=500)
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def save_insurance_draft(request):
|
||
"""
|
||
AJAX view to save insurance form as draft.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
if request.method == 'POST':
|
||
try:
|
||
from .models import InsuranceInfo, PatientProfile
|
||
from .forms import InsuranceInfoForm
|
||
import json
|
||
|
||
# Get form data
|
||
form_data = request.POST.copy()
|
||
|
||
# Check if this is an update to existing insurance or new draft
|
||
insurance_id = form_data.get('insurance_id')
|
||
patient_id = form_data.get('patient')
|
||
|
||
if not patient_id:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Patient ID is required'
|
||
})
|
||
|
||
# Get patient
|
||
patient = get_object_or_404(PatientProfile, id=patient_id, tenant=tenant)
|
||
|
||
# Prepare draft data
|
||
draft_data = {
|
||
'patient_id': patient_id,
|
||
'insurance_provider': form_data.get('insurance_provider', ''),
|
||
'policy_number': form_data.get('policy_number', ''),
|
||
'member_id': form_data.get('member_id', ''),
|
||
'group_number': form_data.get('group_number', ''),
|
||
'is_primary': form_data.get('is_primary') == 'on',
|
||
'effective_date': form_data.get('effective_date', ''),
|
||
'expiration_date': form_data.get('expiration_date', ''),
|
||
'copay': form_data.get('copay', ''),
|
||
'deductible': form_data.get('deductible', ''),
|
||
'notes': form_data.get('notes', ''),
|
||
'subscriber_name': form_data.get('subscriber_name', ''),
|
||
'subscriber_relationship': form_data.get('subscriber_relationship', ''),
|
||
'subscriber_dob': form_data.get('subscriber_dob', ''),
|
||
'subscriber_ssn': form_data.get('subscriber_ssn', ''),
|
||
'employer': form_data.get('employer', ''),
|
||
'plan_type': form_data.get('plan_type', ''),
|
||
'network_type': form_data.get('network_type', ''),
|
||
'prior_authorization_required': form_data.get('prior_authorization_required') == 'on',
|
||
'saved_at': timezone.now().isoformat(),
|
||
'saved_by': request.user.id,
|
||
}
|
||
|
||
# Try to create or update insurance record as draft
|
||
if insurance_id:
|
||
# Update existing insurance
|
||
insurance = get_object_or_404(InsuranceInfo, id=insurance_id, patient__tenant=tenant)
|
||
|
||
# Update fields with non-empty values
|
||
for field, value in draft_data.items():
|
||
if field not in ['patient_id', 'saved_at', 'saved_by'] and value:
|
||
if field == 'is_primary' or field == 'prior_authorization_required':
|
||
setattr(insurance, field, value)
|
||
elif field in ['effective_date', 'expiration_date', 'subscriber_dob']:
|
||
if value:
|
||
try:
|
||
from datetime import datetime
|
||
date_value = datetime.strptime(value, '%Y-%m-%d').date()
|
||
setattr(insurance, field, date_value)
|
||
except ValueError:
|
||
pass # Skip invalid dates
|
||
elif field in ['copay', 'deductible']:
|
||
if value:
|
||
try:
|
||
numeric_value = float(value)
|
||
setattr(insurance, field, numeric_value)
|
||
except ValueError:
|
||
pass # Skip invalid numbers
|
||
else:
|
||
setattr(insurance, field, value)
|
||
|
||
# Mark as draft
|
||
if hasattr(insurance, 'status'):
|
||
insurance.status = 'draft'
|
||
|
||
insurance.save()
|
||
|
||
action = 'Update Insurance Draft'
|
||
message = f'Updated insurance draft for {patient}'
|
||
|
||
else:
|
||
# Create new insurance record as draft
|
||
insurance_data = {}
|
||
|
||
# Map form fields to model fields
|
||
for field, value in draft_data.items():
|
||
if field not in ['patient_id', 'saved_at', 'saved_by'] and value:
|
||
if field in ['effective_date', 'expiration_date', 'subscriber_dob']:
|
||
if value:
|
||
try:
|
||
from datetime import datetime
|
||
insurance_data[field] = datetime.strptime(value, '%Y-%m-%d').date()
|
||
except ValueError:
|
||
pass # Skip invalid dates
|
||
elif field in ['copay', 'deductible']:
|
||
if value:
|
||
try:
|
||
insurance_data[field] = float(value)
|
||
except ValueError:
|
||
pass # Skip invalid numbers
|
||
else:
|
||
insurance_data[field] = value
|
||
|
||
# Create insurance record
|
||
insurance = InsuranceInfo.objects.create(
|
||
patient=patient,
|
||
**insurance_data
|
||
)
|
||
|
||
# Mark as draft
|
||
if hasattr(insurance, 'status'):
|
||
insurance.status = 'draft'
|
||
insurance.save()
|
||
|
||
action = 'Create Insurance Draft'
|
||
message = f'Created insurance draft for {patient}'
|
||
|
||
# Store draft metadata (if you have a separate draft tracking system)
|
||
try:
|
||
# This would be used if you have a separate drafts table
|
||
from django.core.cache import cache
|
||
draft_key = f'insurance_draft_{patient.id}_{request.user.id}'
|
||
cache.set(draft_key, json.dumps(draft_data), timeout=86400) # 24 hours
|
||
except:
|
||
pass # Cache not available or other error
|
||
|
||
# Log draft save
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='CREATE' if not insurance_id else 'UPDATE',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action=action,
|
||
description=message,
|
||
user=request.user,
|
||
content_object=insurance,
|
||
request=request
|
||
)
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': 'Draft saved successfully',
|
||
'insurance_id': insurance.id,
|
||
'saved_at': timezone.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||
'draft_data': {
|
||
'provider': draft_data.get('insurance_provider', ''),
|
||
'policy_number': draft_data.get('policy_number', ''),
|
||
'is_primary': draft_data.get('is_primary', False),
|
||
'has_required_fields': bool(
|
||
draft_data.get('insurance_provider') and
|
||
draft_data.get('policy_number') and
|
||
draft_data.get('member_id')
|
||
)
|
||
}
|
||
})
|
||
|
||
except PatientProfile.DoesNotExist:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Patient not found'
|
||
}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': f'Error saving draft: {str(e)}'
|
||
}, status=500)
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
@login_required
|
||
def verify_with_provider(request):
|
||
"""
|
||
AJAX view to verify insurance information directly with the provider.
|
||
"""
|
||
tenant = getattr(request, 'tenant', None)
|
||
if not tenant:
|
||
return JsonResponse({'error': 'No tenant found'}, status=400)
|
||
|
||
if request.method == 'POST':
|
||
provider = request.POST.get('provider', '').strip()
|
||
policy_number = request.POST.get('policy_number', '').strip()
|
||
|
||
if not provider or not policy_number:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Provider and policy number are required'
|
||
})
|
||
|
||
try:
|
||
import random
|
||
from datetime import datetime, timedelta
|
||
import time
|
||
|
||
# Simulate API call delay
|
||
time.sleep(2)
|
||
|
||
# Generate mock verification results
|
||
verification_statuses = ['verified', 'not_found', 'expired', 'suspended', 'pending']
|
||
verification_status = random.choice(verification_statuses)
|
||
|
||
# Base verification data
|
||
verification_data = {
|
||
'status': verification_status,
|
||
'provider': provider,
|
||
'policy_number': policy_number,
|
||
'verification_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||
'verification_id': f'VER{random.randint(100000, 999999)}',
|
||
'response_time': f'{random.uniform(1.5, 3.0):.1f}s',
|
||
}
|
||
|
||
if verification_status == 'verified':
|
||
# Generate detailed verification data for verified policies
|
||
effective_date = datetime.now() - timedelta(days=random.randint(30, 730))
|
||
expiration_date = effective_date + timedelta(days=365)
|
||
|
||
verification_data.update({
|
||
'policy_holder': {
|
||
'name': f'{random.choice(["John", "Jane", "Michael", "Sarah", "David", "Lisa"])} {random.choice(["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"])}',
|
||
'dob': (datetime.now() - timedelta(days=random.randint(6570, 25550))).strftime('%Y-%m-%d'),
|
||
# 18-70 years old
|
||
'member_id': f'MEM{random.randint(100000000, 999999999)}',
|
||
'relationship': random.choice(['self', 'spouse', 'child', 'parent'])
|
||
},
|
||
'coverage_details': {
|
||
'effective_date': effective_date.strftime('%Y-%m-%d'),
|
||
'expiration_date': expiration_date.strftime('%Y-%m-%d'),
|
||
'plan_name': f'{provider} {random.choice(["Gold", "Silver", "Bronze", "Platinum", "Premium"])} Plan',
|
||
'plan_type': random.choice(['HMO', 'PPO', 'EPO', 'POS']),
|
||
'group_number': f'GRP{random.randint(10000, 99999)}',
|
||
'network': random.choice(['In-Network', 'Out-of-Network', 'Preferred Provider'])
|
||
},
|
||
'benefits': {
|
||
'deductible': random.choice([500, 1000, 1500, 2000, 2500, 5000]),
|
||
'out_of_pocket_max': random.choice([3000, 5000, 7500, 10000, 12500]),
|
||
'copay_primary': random.choice([10, 15, 20, 25, 30]),
|
||
'copay_specialist': random.choice([25, 35, 45, 50, 75]),
|
||
'coinsurance': random.choice([10, 15, 20, 25, 30]),
|
||
'prescription_coverage': random.choice([True, False]),
|
||
'dental_coverage': random.choice([True, False]),
|
||
'vision_coverage': random.choice([True, False])
|
||
},
|
||
'authorization': {
|
||
'prior_auth_required': random.choice([True, False]),
|
||
'referral_required': random.choice([True, False]),
|
||
'pre_certification_required': random.choice([True, False])
|
||
},
|
||
'contact_info': {
|
||
'customer_service': f'1-800-{random.randint(100, 999)}-{random.randint(1000, 9999)}',
|
||
'provider_services': f'1-800-{random.randint(100, 999)}-{random.randint(1000, 9999)}',
|
||
'website': f'www.{provider.lower().replace(" ", "")}.com',
|
||
'claims_address': f'{random.randint(100, 9999)} Insurance Blvd, Claims City, ST {random.randint(10000, 99999)}'
|
||
}
|
||
})
|
||
|
||
elif verification_status == 'not_found':
|
||
verification_data.update({
|
||
'error_details': {
|
||
'code': 'POLICY_NOT_FOUND',
|
||
'message': 'Policy number not found in provider database',
|
||
'suggestions': [
|
||
'Verify the policy number is correct',
|
||
'Check if the policy has been recently issued',
|
||
'Contact the insurance provider directly'
|
||
]
|
||
}
|
||
})
|
||
|
||
elif verification_status == 'expired':
|
||
expiration_date = datetime.now() - timedelta(days=random.randint(1, 365))
|
||
verification_data.update({
|
||
'policy_holder': {
|
||
'name': f'{random.choice(["John", "Jane", "Michael", "Sarah"])} {random.choice(["Smith", "Johnson", "Williams", "Brown"])}',
|
||
'member_id': f'MEM{random.randint(100000000, 999999999)}'
|
||
},
|
||
'expiration_details': {
|
||
'expired_date': expiration_date.strftime('%Y-%m-%d'),
|
||
'days_expired': (datetime.now() - expiration_date).days,
|
||
'renewal_options': [
|
||
'Contact employer for renewal',
|
||
'Apply for individual coverage',
|
||
'Check for COBRA eligibility'
|
||
]
|
||
}
|
||
})
|
||
|
||
elif verification_status == 'suspended':
|
||
verification_data.update({
|
||
'suspension_details': {
|
||
'reason': random.choice(['Non-payment', 'Fraud investigation', 'Administrative review']),
|
||
'suspended_date': (datetime.now() - timedelta(days=random.randint(1, 90))).strftime('%Y-%m-%d'),
|
||
'reinstatement_required': True,
|
||
'contact_required': True
|
||
}
|
||
})
|
||
|
||
elif verification_status == 'pending':
|
||
verification_data.update({
|
||
'pending_details': {
|
||
'reason': random.choice(
|
||
['New enrollment processing', 'Coverage change in progress', 'System update in progress']),
|
||
'expected_resolution': (datetime.now() + timedelta(days=random.randint(1, 14))).strftime(
|
||
'%Y-%m-%d'),
|
||
'reference_number': f'REF{random.randint(100000, 999999)}'
|
||
}
|
||
})
|
||
|
||
# Log verification attempt
|
||
AuditLogger.log_event(
|
||
tenant=tenant,
|
||
event_type='VERIFY',
|
||
event_category='PATIENT_MANAGEMENT',
|
||
action='Verify Insurance with Provider',
|
||
description=f'Verified insurance {policy_number} with {provider} - Status: {verification_status}',
|
||
user=request.user,
|
||
request=request
|
||
)
|
||
|
||
# Render the verification results template
|
||
html = render(request, 'patients/partials/provider_verification_results.html', {
|
||
'verification': verification_data
|
||
}).content.decode('utf-8')
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'status': verification_status,
|
||
'html': html,
|
||
'verification_id': verification_data['verification_id']
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': f'Error verifying with provider: {str(e)}'
|
||
}, status=500)
|
||
|
||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||
|
||
|
||
# Insurance Claims Views
|
||
from .models import InsuranceClaim, ClaimDocument, ClaimStatusHistory
|
||
from django.core.paginator import Paginator
|
||
from django.db.models import Q, Sum, Count, Avg
|
||
from django.db.models.functions import TruncMonth
|
||
import json
|
||
|
||
|
||
@login_required
|
||
def insurance_claims_list(request):
|
||
"""List all insurance claims with filtering and search."""
|
||
claims = InsuranceClaim.objects.filter(
|
||
patient__tenant=request.user.tenant
|
||
).select_related('patient', 'insurance_info').order_by('-created_at')
|
||
|
||
# Search functionality
|
||
search_query = request.GET.get('search', '')
|
||
if search_query:
|
||
claims = claims.filter(
|
||
Q(claim_number__icontains=search_query) |
|
||
Q(patient__first_name__icontains=search_query) |
|
||
Q(patient__last_name__icontains=search_query) |
|
||
Q(service_provider__icontains=search_query) |
|
||
Q(facility_name__icontains=search_query)
|
||
)
|
||
|
||
# Filter by status
|
||
status_filter = request.GET.get('status', '')
|
||
if status_filter:
|
||
claims = claims.filter(status=status_filter)
|
||
|
||
# Filter by claim type
|
||
type_filter = request.GET.get('type', '')
|
||
if type_filter:
|
||
claims = claims.filter(claim_type=type_filter)
|
||
|
||
# Filter by priority
|
||
priority_filter = request.GET.get('priority', '')
|
||
if priority_filter:
|
||
claims = claims.filter(priority=priority_filter)
|
||
|
||
# Date range filter
|
||
date_from = request.GET.get('date_from', '')
|
||
date_to = request.GET.get('date_to', '')
|
||
if date_from:
|
||
claims = claims.filter(service_date__gte=date_from)
|
||
if date_to:
|
||
claims = claims.filter(service_date__lte=date_to)
|
||
|
||
# Calculate statistics
|
||
stats = claims.aggregate(
|
||
total_claims=Count('id'),
|
||
total_billed=Sum('billed_amount'),
|
||
total_approved=Sum('approved_amount'),
|
||
total_paid=Sum('paid_amount'),
|
||
avg_amount=Avg('billed_amount')
|
||
)
|
||
|
||
# Status distribution
|
||
status_counts = claims.values('status').annotate(count=Count('id'))
|
||
|
||
# Pagination
|
||
paginator = Paginator(claims, 20)
|
||
page_number = request.GET.get('page')
|
||
page_obj = paginator.get_page(page_number)
|
||
|
||
context = {
|
||
'claims': page_obj,
|
||
'search_query': search_query,
|
||
'status_filter': status_filter,
|
||
'type_filter': type_filter,
|
||
'priority_filter': priority_filter,
|
||
'date_from': date_from,
|
||
'date_to': date_to,
|
||
'stats': stats,
|
||
'status_counts': status_counts,
|
||
'status_choices': InsuranceClaim.STATUS_CHOICES,
|
||
'type_choices': InsuranceClaim.CLAIM_TYPE_CHOICES,
|
||
'priority_choices': InsuranceClaim.PRIORITY_CHOICES,
|
||
}
|
||
|
||
return render(request, 'patients/claims/insurance_claims_list.html', context)
|
||
|
||
|
||
@login_required
|
||
def insurance_claim_detail(request, claim_id):
|
||
"""Display detailed view of an insurance claim."""
|
||
claim = get_object_or_404(
|
||
InsuranceClaim.objects.select_related('patient', 'insurance_info', 'created_by'),
|
||
id=claim_id,
|
||
patient__tenant=request.user.tenant
|
||
)
|
||
|
||
# Get claim documents
|
||
documents = claim.documents.all().order_by('-uploaded_at')
|
||
|
||
# Get status history
|
||
status_history = claim.status_history.all().select_related('changed_by').order_by('-changed_at')
|
||
|
||
# Calculate approval percentage
|
||
approval_percentage = 0
|
||
if claim.billed_amount > 0:
|
||
approval_percentage = (claim.approved_amount / claim.billed_amount) * 100
|
||
|
||
context = {
|
||
'claim': claim,
|
||
'documents': documents,
|
||
'status_history': status_history,
|
||
'approval_percentage': approval_percentage,
|
||
}
|
||
|
||
return render(request, 'patients/claims/insurance_claim_detail.html', context)
|
||
|
||
|
||
@login_required
|
||
def insurance_claim_form(request, claim_id=None):
|
||
"""Create or edit an insurance claim."""
|
||
claim = None
|
||
if claim_id:
|
||
claim = get_object_or_404(
|
||
InsuranceClaim,
|
||
id=claim_id,
|
||
patient__tenant=request.user.tenant
|
||
)
|
||
|
||
if request.method == 'POST':
|
||
try:
|
||
# Get form data
|
||
patient_id = request.POST.get('patient')
|
||
insurance_id = request.POST.get('insurance_info')
|
||
|
||
# Validate patient and insurance
|
||
patient = get_object_or_404(
|
||
PatientProfile,
|
||
id=patient_id,
|
||
tenant=request.user.tenant
|
||
)
|
||
|
||
insurance_info = get_object_or_404(
|
||
InsuranceInfo,
|
||
id=insurance_id,
|
||
patient=patient
|
||
)
|
||
|
||
# Prepare claim data
|
||
claim_data = {
|
||
'patient': patient,
|
||
'insurance_info': insurance_info,
|
||
'claim_type': request.POST.get('claim_type'),
|
||
'priority': request.POST.get('priority', 'NORMAL'),
|
||
'service_date': request.POST.get('service_date'),
|
||
'service_provider': request.POST.get('service_provider'),
|
||
'service_provider_license': request.POST.get('service_provider_license'),
|
||
'facility_name': request.POST.get('facility_name'),
|
||
'facility_license': request.POST.get('facility_license'),
|
||
'primary_diagnosis_code': request.POST.get('primary_diagnosis_code'),
|
||
'primary_diagnosis_description': request.POST.get('primary_diagnosis_description'),
|
||
'billed_amount': request.POST.get('billed_amount'),
|
||
'saudi_id_number': request.POST.get('saudi_id_number'),
|
||
'insurance_card_number': request.POST.get('insurance_card_number'),
|
||
'authorization_number': request.POST.get('authorization_number'),
|
||
'notes': request.POST.get('notes'),
|
||
}
|
||
|
||
# Handle JSON fields
|
||
secondary_diagnoses = request.POST.get('secondary_diagnosis_codes', '[]')
|
||
procedures = request.POST.get('procedure_codes', '[]')
|
||
attachments = request.POST.get('attachments', '[]')
|
||
|
||
try:
|
||
claim_data['secondary_diagnosis_codes'] = json.loads(secondary_diagnoses)
|
||
claim_data['procedure_codes'] = json.loads(procedures)
|
||
claim_data['attachments'] = json.loads(attachments)
|
||
except json.JSONDecodeError:
|
||
claim_data['secondary_diagnosis_codes'] = []
|
||
claim_data['procedure_codes'] = []
|
||
claim_data['attachments'] = []
|
||
|
||
if claim:
|
||
# Update existing claim
|
||
for key, value in claim_data.items():
|
||
setattr(claim, key, value)
|
||
claim.save()
|
||
|
||
# Create status history entry
|
||
ClaimStatusHistory.objects.create(
|
||
claim=claim,
|
||
from_status=claim.status,
|
||
to_status=claim.status,
|
||
reason='Claim updated',
|
||
changed_by=request.user
|
||
)
|
||
|
||
messages.success(request, f'Claim {claim.claim_number} updated successfully.')
|
||
else:
|
||
# Create new claim
|
||
claim_data['created_by'] = request.user
|
||
claim = InsuranceClaim.objects.create(**claim_data)
|
||
|
||
# Create initial status history
|
||
ClaimStatusHistory.objects.create(
|
||
claim=claim,
|
||
from_status=None,
|
||
to_status='DRAFT',
|
||
reason='Initial claim creation',
|
||
changed_by=request.user
|
||
)
|
||
|
||
messages.success(request, f'Claim {claim.claim_number} created successfully.')
|
||
|
||
return redirect('patients:insurance_claim_detail', claim_id=claim.id)
|
||
|
||
except Exception as e:
|
||
messages.error(request, f'Error saving claim: {str(e)}')
|
||
|
||
# Get patients and their insurance for form
|
||
patients = PatientProfile.objects.filter(tenant=request.user.tenant).order_by('last_name', 'first_name')
|
||
|
||
context = {
|
||
'claim': claim,
|
||
'patients': patients,
|
||
'status_choices': InsuranceClaim.STATUS_CHOICES,
|
||
'type_choices': InsuranceClaim.CLAIM_TYPE_CHOICES,
|
||
'priority_choices': InsuranceClaim.PRIORITY_CHOICES,
|
||
}
|
||
|
||
return render(request, 'patients/claims/insurance_claim_form.html', context)
|
||
|
||
|
||
@login_required
|
||
def insurance_claim_delete(request, claim_id):
|
||
"""Delete an insurance claim."""
|
||
claim = get_object_or_404(
|
||
InsuranceClaim,
|
||
id=claim_id,
|
||
patient__tenant=request.user.tenant
|
||
)
|
||
|
||
if request.method == 'POST':
|
||
claim_number = claim.claim_number
|
||
claim.delete()
|
||
messages.success(request, f'Claim {claim_number} deleted successfully.')
|
||
return redirect('patients:insurance_claims_list')
|
||
|
||
context = {'claim': claim}
|
||
return render(request, 'patients/claims/insurance_claim_confirm_delete.html', context)
|
||
|
||
|
||
@login_required
|
||
def update_claim_status(request, claim_id):
|
||
"""Update claim status via AJAX."""
|
||
if request.method != 'POST':
|
||
return JsonResponse({'success': False, 'error': 'Invalid method'})
|
||
|
||
claim = get_object_or_404(
|
||
InsuranceClaim,
|
||
id=claim_id,
|
||
patient__tenant=request.user.tenant
|
||
)
|
||
|
||
new_status = request.POST.get('status')
|
||
reason = request.POST.get('reason', '')
|
||
|
||
if new_status not in dict(InsuranceClaim.STATUS_CHOICES):
|
||
return JsonResponse({'success': False, 'error': 'Invalid status'})
|
||
|
||
old_status = claim.status
|
||
claim.status = new_status
|
||
|
||
# Auto-set dates based on status
|
||
if new_status == 'SUBMITTED' and not claim.submitted_date:
|
||
claim.submitted_date = timezone.now()
|
||
elif new_status in ['APPROVED', 'PARTIALLY_APPROVED', 'DENIED'] and not claim.processed_date:
|
||
claim.processed_date = timezone.now()
|
||
elif new_status == 'PAID' and not claim.payment_date:
|
||
claim.payment_date = timezone.now()
|
||
|
||
claim.save()
|
||
|
||
# Create status history entry
|
||
ClaimStatusHistory.objects.create(
|
||
claim=claim,
|
||
from_status=old_status,
|
||
to_status=new_status,
|
||
reason=reason or f'Status changed from {old_status} to {new_status}',
|
||
changed_by=request.user
|
||
)
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': f'Claim status updated to {new_status}',
|
||
'new_status': new_status,
|
||
'status_display': claim.get_status_display()
|
||
})
|
||
|
||
|
||
@login_required
|
||
def claims_dashboard(request):
|
||
"""Dashboard view for insurance claims analytics."""
|
||
claims = InsuranceClaim.objects.filter(patient__tenant=request.user.tenant)
|
||
|
||
# Overall statistics
|
||
stats = claims.aggregate(
|
||
total_claims=Count('id'),
|
||
total_billed=Sum('billed_amount'),
|
||
total_approved=Sum('approved_amount'),
|
||
total_paid=Sum('paid_amount'),
|
||
avg_processing_days=Avg('processing_time_days')
|
||
)
|
||
|
||
# Status distribution
|
||
status_distribution = claims.values('status').annotate(
|
||
count=Count('id'),
|
||
total_amount=Sum('billed_amount')
|
||
).order_by('status')
|
||
|
||
# Claims by type
|
||
type_distribution = claims.values('claim_type').annotate(
|
||
count=Count('id'),
|
||
total_amount=Sum('billed_amount')
|
||
).order_by('-count')
|
||
|
||
# Monthly trends (last 12 months)
|
||
monthly_trends = claims.filter(
|
||
service_date__gte=timezone.now().date() - timedelta(days=365)
|
||
).annotate(
|
||
month=TruncMonth('service_date')
|
||
).values('month').annotate(
|
||
count=Count('id'),
|
||
total_billed=Sum('billed_amount'),
|
||
total_approved=Sum('approved_amount')
|
||
).order_by('month')
|
||
|
||
# Top providers
|
||
top_providers = claims.values('service_provider').annotate(
|
||
count=Count('id'),
|
||
total_amount=Sum('billed_amount')
|
||
).order_by('-count')[:10]
|
||
|
||
# Recent claims
|
||
recent_claims = claims.select_related('patient', 'insurance_info').order_by('-created_at')[:10]
|
||
|
||
# Pending claims requiring attention
|
||
pending_claims = claims.filter(
|
||
status__in=['SUBMITTED', 'UNDER_REVIEW']
|
||
).select_related('patient').order_by('submitted_date')[:10]
|
||
|
||
context = {
|
||
'stats': stats,
|
||
'status_distribution': status_distribution,
|
||
'type_distribution': type_distribution,
|
||
'monthly_trends': list(monthly_trends),
|
||
'top_providers': top_providers,
|
||
'recent_claims': recent_claims,
|
||
'pending_claims': pending_claims,
|
||
}
|
||
|
||
return render(request, 'patients/claims/claims_dashboard.html', context)
|
||
|
||
|
||
@login_required
|
||
def get_patient_insurance(request, patient_id):
|
||
"""Get insurance policies for a patient via AJAX."""
|
||
try:
|
||
patient = PatientProfile.objects.get(
|
||
id=patient_id,
|
||
tenant=request.user.tenant
|
||
)
|
||
|
||
insurance_policies = patient.insurance_info.all().values(
|
||
'id', 'insurance_company', 'policy_number', 'insurance_type'
|
||
)
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'insurance_policies': list(insurance_policies)
|
||
})
|
||
|
||
except PatientProfile.DoesNotExist:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Patient not found'
|
||
})
|
||
|
||
|
||
@login_required
|
||
def bulk_claim_actions(request):
|
||
"""Handle bulk actions on claims."""
|
||
if request.method != 'POST':
|
||
return JsonResponse({'success': False, 'error': 'Invalid method'})
|
||
|
||
action = request.POST.get('action')
|
||
claim_ids = request.POST.getlist('claim_ids')
|
||
|
||
if not claim_ids:
|
||
return JsonResponse({'success': False, 'error': 'No claims selected'})
|
||
|
||
claims = InsuranceClaim.objects.filter(
|
||
id__in=claim_ids,
|
||
patient__tenant=request.user.tenant
|
||
)
|
||
|
||
if action == 'update_status':
|
||
new_status = request.POST.get('new_status')
|
||
if new_status not in dict(InsuranceClaim.STATUS_CHOICES):
|
||
return JsonResponse({'success': False, 'error': 'Invalid status'})
|
||
|
||
updated_count = 0
|
||
for claim in claims:
|
||
old_status = claim.status
|
||
claim.status = new_status
|
||
claim.save()
|
||
|
||
# Create status history
|
||
ClaimStatusHistory.objects.create(
|
||
claim=claim,
|
||
from_status=old_status,
|
||
to_status=new_status,
|
||
reason=f'Bulk status update to {new_status}',
|
||
changed_by=request.user
|
||
)
|
||
updated_count += 1
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': f'Updated {updated_count} claims to {new_status}'
|
||
})
|
||
|
||
elif action == 'export':
|
||
# Export claims to CSV
|
||
import csv
|
||
from django.http import HttpResponse
|
||
|
||
response = HttpResponse(content_type='text/csv')
|
||
response['Content-Disposition'] = 'attachment; filename="insurance_claims.csv"'
|
||
|
||
writer = csv.writer(response)
|
||
writer.writerow([
|
||
'Claim Number', 'Patient Name', 'Service Date', 'Provider',
|
||
'Claim Type', 'Status', 'Billed Amount', 'Approved Amount'
|
||
])
|
||
|
||
for claim in claims.select_related('patient'):
|
||
writer.writerow([
|
||
claim.claim_number,
|
||
claim.patient.get_full_name(),
|
||
claim.service_date,
|
||
claim.service_provider,
|
||
claim.get_claim_type_display(),
|
||
claim.get_status_display(),
|
||
claim.billed_amount,
|
||
claim.approved_amount
|
||
])
|
||
|
||
return response
|
||
|
||
return JsonResponse({'success': False, 'error': 'Invalid action'})
|
||
|