Marwan Alwali 43901b5bda update
2025-09-09 01:15:48 +03:00

4714 lines
173 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
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 patients 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'})