""" 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 in the URL). """ model = EmergencyContact form_class = EmergencyContactForm template_name = "patients/emergency_contacts/emergency_contact_form.html" permission_required = "patients.add_emergencycontact" # if you always go back to the list, keep this; otherwise override get_success_url below success_url = reverse_lazy("patients:emergency_contact_list") def dispatch(self, request, *args, **kwargs): # Ensure tenant exists (if you follow multi-tenant pattern) self.tenant = getattr(request, "tenant", None) if not self.tenant: return JsonResponse({"error": "No tenant found"}, status=400) # Fetch the patient from URL and ensure it belongs to the tenant self.patient = get_object_or_404( PatientProfile, pk=self.kwargs.get("pk"), tenant=self.tenant, is_active=True, ) return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() # pass user for your form's tenant scoping kwargs["user"] = self.request.user # prefill the patient kwargs.setdefault("initial", {}) kwargs["initial"]["patient"] = self.patient return kwargs def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx["patient"] = self.patient return ctx def form_valid(self, form): # Enforce the patient from the URL regardless of any posted value form.instance.patient = self.patient response = super().form_valid(form) # Log emergency contact creation (fix name fields) contact_name = f"{self.object.first_name} {self.object.last_name}".strip() AuditLogger.log_event( tenant=self.tenant, event_type="CREATE", event_category="PATIENT_MANAGEMENT", action="Create Emergency Contact", description=f'Created emergency contact: {contact_name} for patient {self.patient}', user=self.request.user, content_object=self.object, request=self.request, ) messages.success(self.request, f'Emergency contact "{contact_name}" created successfully.') return response # Optional: redirect back to the patient’s detail page (or their contact list) def get_success_url(self): return reverse("patients:patient_detail", args=[self.patient.pk]) class EmergencyContactUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update emergency contact. """ model = EmergencyContact form_class = EmergencyContactForm template_name = 'patients/emergency_contacts/emergency_contact_form.html' permission_required = 'patients.change_emergencycontact' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return EmergencyContact.objects.none() return EmergencyContact.objects.filter(patient__tenant=tenant) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_success_url(self): return reverse('patients:emergency_contact_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log emergency contact update AuditLogger.log_event( tenant=getattr(self.request, 'tenant', None), event_type='UPDATE', event_category='PATIENT_MANAGEMENT', action='Update Emergency Contact', description=f'Updated emergency contact: {self.object.name}', user=self.request.user, content_object=self.object, request=self.request ) messages.success(self.request, f'Emergency contact "{self.object.name}" updated successfully.') return response class EmergencyContactDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ Delete emergency contact. """ model = EmergencyContact template_name = 'patients/emergency_contacts/emergency_contact_confirm_delete.html' permission_required = 'patients.delete_emergencycontact' success_url = reverse_lazy('patients:emergency_contact_list') def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return EmergencyContact.objects.none() return EmergencyContact.objects.filter(patient__tenant=tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() contact_name = self.object.name # Log emergency contact deletion AuditLogger.log_event( tenant=getattr(request, 'tenant', None), event_type='DELETE', event_category='PATIENT_MANAGEMENT', action='Delete Emergency Contact', description=f'Deleted emergency contact: {contact_name}', user=request.user, content_object=self.object, request=request ) messages.success(request, f'Emergency contact "{contact_name}" deleted successfully.') return super().delete(request, *args, **kwargs) class InsuranceInfoListView(LoginRequiredMixin, ListView): """ List insurance information, optionally scoped to a specific patient via URL . 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//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 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'})