1169 lines
45 KiB
Python
1169 lines
45 KiB
Python
"""
|
|
Medical views for the Tenhal Multidisciplinary Healthcare Platform.
|
|
|
|
This module contains views for medical documentation including:
|
|
- Medical consultations (MD-F-1)
|
|
- Follow-up visits (MD-F-2)
|
|
- Medication management
|
|
- Lab/radiology integration
|
|
"""
|
|
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.db.models import Q
|
|
from django.http import HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
|
|
from django.urls import reverse_lazy
|
|
from django.contrib import messages
|
|
|
|
from core.mixins import (
|
|
SignedDocumentEditPreventionMixin,
|
|
TenantFilterMixin,
|
|
RolePermissionMixin,
|
|
AuditLogMixin,
|
|
HTMXResponseMixin,
|
|
SuccessMessageMixin,
|
|
PaginationMixin,
|
|
ConsentRequiredMixin,
|
|
SignedDocumentEditPreventionMixin,
|
|
)
|
|
from core.models import User, Patient
|
|
from appointments.models import Appointment
|
|
from .models import MedicalConsultation, MedicalFollowUp, MedicationPlan, ConsultationResponse, ConsultationFeedback
|
|
from .forms import (
|
|
MedicalConsultationForm, MedicalFollowUpForm, MedicationPlanFormSet,
|
|
ConsultationResponseForm, ConsultationFeedbackForm
|
|
)
|
|
|
|
|
|
# Sign Views
|
|
class MedicalConsultationSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
|
|
"""Sign a medical consultation."""
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
|
|
|
|
def post(self, request, pk):
|
|
consultation = get_object_or_404(MedicalConsultation, pk=pk, tenant=request.user.tenant)
|
|
if consultation.signed_by:
|
|
messages.warning(request, _("This consultation has already been signed."))
|
|
return HttpResponseRedirect(reverse_lazy('medical:consultation_detail', kwargs={'pk': pk}))
|
|
if consultation.provider != request.user and request.user.role != User.Role.ADMIN:
|
|
messages.error(request, _("Only the consultation provider or an administrator can sign this consultation."))
|
|
return HttpResponseRedirect(reverse_lazy('medical:consultation_detail', kwargs={'pk': pk}))
|
|
consultation.signed_by = request.user
|
|
consultation.signed_at = timezone.now()
|
|
consultation.save()
|
|
messages.success(request, _("Consultation signed successfully!"))
|
|
return HttpResponseRedirect(reverse_lazy('medical:consultation_detail', kwargs={'pk': pk}))
|
|
|
|
|
|
class MedicalFollowUpSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
|
|
"""Sign a medical follow-up."""
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
|
|
|
|
def post(self, request, pk):
|
|
followup = get_object_or_404(MedicalFollowUp, pk=pk, tenant=request.user.tenant)
|
|
if followup.signed_by:
|
|
messages.warning(request, _("This follow-up has already been signed."))
|
|
return HttpResponseRedirect(reverse_lazy('medical:followup_detail', kwargs={'pk': pk}))
|
|
if followup.provider != request.user and request.user.role != User.Role.ADMIN:
|
|
messages.error(request, _("Only the follow-up provider or an administrator can sign this follow-up."))
|
|
return HttpResponseRedirect(reverse_lazy('medical:followup_detail', kwargs={'pk': pk}))
|
|
followup.signed_by = request.user
|
|
followup.signed_at = timezone.now()
|
|
followup.save()
|
|
messages.success(request, _("Follow-up signed successfully!"))
|
|
return HttpResponseRedirect(reverse_lazy('medical:followup_detail', kwargs={'pk': pk}))
|
|
|
|
|
|
class MedicalConsultationListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
|
|
HTMXResponseMixin, ListView):
|
|
"""
|
|
Medical consultation list view (MD-F-1).
|
|
|
|
Features:
|
|
- Filter by patient, provider, date range
|
|
- Search by patient name/MRN
|
|
- Role-based filtering (doctors see their consultations)
|
|
"""
|
|
model = MedicalConsultation
|
|
template_name = 'medical/consultation_list.html'
|
|
htmx_template_name = 'medical/partials/consultation_list_partial.html'
|
|
context_object_name = 'consultations'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
"""Get filtered queryset."""
|
|
queryset = super().get_queryset()
|
|
user = self.request.user
|
|
|
|
# Role-based filtering
|
|
if user.role == User.Role.DOCTOR:
|
|
queryset = queryset.filter(provider__user=user)
|
|
|
|
# Apply search
|
|
search_query = self.request.GET.get('search', '').strip()
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(patient__first_name_en__icontains=search_query) |
|
|
Q(patient__last_name_en__icontains=search_query) |
|
|
Q(patient__mrn__icontains=search_query)
|
|
)
|
|
|
|
# Apply filters
|
|
patient_id = self.request.GET.get('patient')
|
|
if patient_id:
|
|
queryset = queryset.filter(patient_id=patient_id)
|
|
|
|
provider_id = self.request.GET.get('provider')
|
|
if provider_id:
|
|
queryset = queryset.filter(provider_id=provider_id)
|
|
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(consultation_date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(consultation_date__lte=date_to)
|
|
|
|
return queryset.select_related('patient', 'provider', 'appointment', 'signed_by').order_by('-consultation_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add filter options and unsigned counts."""
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
|
|
# Add providers for filter
|
|
context['providers'] = User.objects.filter(
|
|
tenant=self.request.user.tenant,
|
|
role=User.Role.DOCTOR
|
|
)
|
|
|
|
# Get unsigned consultations
|
|
unsigned_query = MedicalConsultation.objects.filter(tenant=user.tenant, signed_by__isnull=True)
|
|
if user.role == User.Role.DOCTOR:
|
|
unsigned_query = unsigned_query.filter(provider=user)
|
|
|
|
context['unsigned_count'] = unsigned_query.count()
|
|
context['unsigned_items'] = unsigned_query.select_related('patient', 'provider').order_by('-consultation_date')[:10]
|
|
|
|
# Add current filters
|
|
context['current_filters'] = {
|
|
'search': self.request.GET.get('search', ''),
|
|
'patient': self.request.GET.get('patient', ''),
|
|
'provider': self.request.GET.get('provider', ''),
|
|
'date_from': self.request.GET.get('date_from', ''),
|
|
'date_to': self.request.GET.get('date_to', ''),
|
|
}
|
|
|
|
return context
|
|
|
|
|
|
class MedicalConsultationDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
|
|
"""
|
|
Medical consultation detail view (MD-F-1).
|
|
|
|
Features:
|
|
- Full consultation details with all sections
|
|
- Medication plans
|
|
- Lab/radiology orders
|
|
- Follow-up consultations
|
|
- Behavioral symptoms checklist
|
|
"""
|
|
model = MedicalConsultation
|
|
template_name = 'medical/consultation_detail.html'
|
|
context_object_name = 'consultation'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add related data."""
|
|
context = super().get_context_data(**kwargs)
|
|
consultation = self.object
|
|
|
|
# Get medication plans (medications is a JSONField, not a related manager)
|
|
context['medications'] = consultation.medications if consultation.medications else []
|
|
|
|
# Get follow-ups
|
|
context['followups'] = MedicalFollowUp.objects.filter(
|
|
patient=consultation.patient,
|
|
previous_consultation=consultation
|
|
).order_by('-followup_date')
|
|
|
|
# Get responses from other disciplines
|
|
context['responses'] = consultation.responses.select_related('responder').order_by('-response_date')
|
|
|
|
# Get feedback
|
|
context['feedback'] = consultation.feedback.select_related('submitted_by').order_by('-feedback_date')
|
|
|
|
# Calculate average feedback rating
|
|
feedback_list = list(context['feedback'])
|
|
if feedback_list:
|
|
avg_ratings = [f.average_rating for f in feedback_list if f.average_rating is not None]
|
|
context['average_feedback_rating'] = sum(avg_ratings) / len(avg_ratings) if avg_ratings else None
|
|
else:
|
|
context['average_feedback_rating'] = None
|
|
|
|
# Get lab/radiology orders (from integrations)
|
|
try:
|
|
from integrations.models import ExternalOrder
|
|
context['lab_orders'] = ExternalOrder.objects.filter(
|
|
patient=consultation.patient,
|
|
order_type='LAB',
|
|
created_at__gte=consultation.consultation_date
|
|
).order_by('-created_at')[:5]
|
|
|
|
context['radiology_orders'] = ExternalOrder.objects.filter(
|
|
patient=consultation.patient,
|
|
order_type='RADIOLOGY',
|
|
created_at__gte=consultation.consultation_date
|
|
).order_by('-created_at')[:5]
|
|
except:
|
|
context['lab_orders'] = []
|
|
context['radiology_orders'] = []
|
|
|
|
return context
|
|
|
|
|
|
class MedicalConsultationCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
|
|
AuditLogMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
Medical consultation creation view (MD-F-1).
|
|
|
|
Features:
|
|
- Complete consultation form with all sections
|
|
- Chief complaint, present history, past history
|
|
- Vaccination, family history, social history
|
|
- Pregnancy/neonatal/developmental history
|
|
- Behavioral symptoms checklist
|
|
- Physical examination
|
|
- Summary and recommendations
|
|
- Medication formset
|
|
- Consent verification enforced before creation
|
|
"""
|
|
model = MedicalConsultation
|
|
form_class = MedicalConsultationForm
|
|
template_name = 'medical/consultation_form.html'
|
|
success_message = _("Medical consultation recorded successfully!")
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
|
|
|
|
# Consent enforcement
|
|
consent_service_type = 'MEDICAL'
|
|
consent_error_message = _(
|
|
"Patient must sign general treatment consent before medical consultation can be documented."
|
|
)
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to consultation detail."""
|
|
return reverse_lazy('medical:consultation_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def get_patient(self):
|
|
"""Get patient for consent verification."""
|
|
patient_id = self.request.GET.get('patient')
|
|
appointment_id = self.request.GET.get('appointment_id')
|
|
|
|
if patient_id:
|
|
return Patient.objects.get(
|
|
pk=patient_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
elif appointment_id:
|
|
appointment = Appointment.objects.get(
|
|
pk=appointment_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
return appointment.patient
|
|
|
|
return None
|
|
|
|
def form_valid(self, form):
|
|
"""Set tenant and provider, handle medications JSON."""
|
|
# Set tenant
|
|
form.instance.tenant = self.request.user.tenant
|
|
|
|
# Set provider
|
|
form.instance.provider = self.request.user
|
|
|
|
# Set consultation date if not provided
|
|
if not form.instance.consultation_date:
|
|
form.instance.consultation_date = timezone.now().date()
|
|
|
|
# Handle medications JSON field
|
|
import json
|
|
medications_json = self.request.POST.get('medications', '[]')
|
|
try:
|
|
form.instance.medications = json.loads(medications_json)
|
|
except json.JSONDecodeError:
|
|
form.instance.medications = []
|
|
|
|
# Save consultation
|
|
self.object = form.save()
|
|
|
|
# Update appointment if linked
|
|
if self.object.appointment:
|
|
# Appointment remains IN_PROGRESS until completed
|
|
pass
|
|
|
|
messages.success(self.request, self.success_message)
|
|
return redirect(self.get_success_url())
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add form title, patient/appointment info, and medication formset."""
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_title'] = _('Medical Consultation (MD-F-1)')
|
|
context['submit_text'] = _('Save Consultation')
|
|
|
|
# Add medication formset if not already in context
|
|
if 'medication_formset' not in context:
|
|
context['medication_formset'] = MedicationPlanFormSet()
|
|
|
|
# Get patient if provided
|
|
patient_id = self.request.GET.get('patient')
|
|
if patient_id:
|
|
try:
|
|
context['patient'] = Patient.objects.get(
|
|
pk=patient_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
except Patient.DoesNotExist:
|
|
pass
|
|
|
|
# Get appointment if provided
|
|
appointment_id = self.request.GET.get('appointment_id')
|
|
if appointment_id:
|
|
try:
|
|
context['appointment'] = Appointment.objects.get(
|
|
pk=appointment_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
context['patient'] = context['appointment'].patient
|
|
except Appointment.DoesNotExist:
|
|
pass
|
|
|
|
return context
|
|
|
|
|
|
class MedicalConsultationUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
|
|
AuditLogMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Medical consultation update view (MD-F-1).
|
|
|
|
Features:
|
|
- Update consultation details
|
|
- Medication formset
|
|
- Version history
|
|
- Audit trail
|
|
"""
|
|
model = MedicalConsultation
|
|
form_class = MedicalConsultationForm
|
|
template_name = 'medical/consultation_form.html'
|
|
success_message = _("Medical consultation updated successfully!")
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to consultation detail."""
|
|
return reverse_lazy('medical:consultation_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def form_valid(self, form):
|
|
"""Save form and handle medications JSON."""
|
|
# Handle medications JSON field
|
|
import json
|
|
medications_json = self.request.POST.get('medications', '[]')
|
|
try:
|
|
form.instance.medications = json.loads(medications_json)
|
|
except json.JSONDecodeError:
|
|
form.instance.medications = []
|
|
|
|
# Save consultation
|
|
self.object = form.save()
|
|
|
|
messages.success(self.request, self.success_message)
|
|
return redirect(self.get_success_url())
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add form title, medication formset, and history."""
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_title'] = _('Update Medical Consultation - %(mrn)s') % {'mrn': self.object.patient.mrn}
|
|
context['submit_text'] = _('Update Consultation')
|
|
context['patient'] = self.object.patient
|
|
|
|
# Add medication formset if not already in context
|
|
if 'medication_formset' not in context:
|
|
context['medication_formset'] = MedicationPlanFormSet(instance=self.object)
|
|
|
|
# Add version history if available
|
|
if hasattr(self.object, 'history'):
|
|
context['history'] = self.object.history.all()[:10]
|
|
|
|
return context
|
|
|
|
|
|
class MedicalFollowUpListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
|
|
HTMXResponseMixin, ListView):
|
|
"""
|
|
Medical follow-up list view (MD-F-2).
|
|
|
|
Features:
|
|
- Filter by patient, provider, date range
|
|
- Search by patient name/MRN
|
|
- Link to previous consultations
|
|
"""
|
|
model = MedicalFollowUp
|
|
template_name = 'medical/followup_list.html'
|
|
htmx_template_name = 'medical/partials/followup_list_partial.html'
|
|
context_object_name = 'followups'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
"""Get filtered queryset."""
|
|
queryset = super().get_queryset()
|
|
user = self.request.user
|
|
|
|
# Role-based filtering
|
|
if user.role == User.Role.DOCTOR:
|
|
queryset = queryset.filter(provider__user=user)
|
|
|
|
# Apply search
|
|
search_query = self.request.GET.get('search', '').strip()
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(patient__first_name_en__icontains=search_query) |
|
|
Q(patient__last_name_en__icontains=search_query) |
|
|
Q(patient__mrn__icontains=search_query)
|
|
)
|
|
|
|
# Apply filters
|
|
patient_id = self.request.GET.get('patient')
|
|
if patient_id:
|
|
queryset = queryset.filter(patient_id=patient_id)
|
|
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(followup_date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(followup_date__lte=date_to)
|
|
|
|
return queryset.select_related(
|
|
'patient', 'provider', 'appointment', 'previous_consultation', 'signed_by'
|
|
).order_by('-followup_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add unsigned follow-ups count."""
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
|
|
unsigned_query = MedicalFollowUp.objects.filter(tenant=user.tenant, signed_by__isnull=True)
|
|
if user.role == User.Role.DOCTOR:
|
|
unsigned_query = unsigned_query.filter(provider=user)
|
|
|
|
context['unsigned_count'] = unsigned_query.count()
|
|
context['unsigned_items'] = unsigned_query.select_related('patient', 'provider').order_by('-followup_date')[:10]
|
|
return context
|
|
|
|
|
|
class MedicalFollowUpDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
|
|
"""
|
|
Medical follow-up detail view (MD-F-2).
|
|
|
|
Features:
|
|
- Full follow-up details
|
|
- Previous consultation reference
|
|
- Previous complaints status
|
|
- New complaints
|
|
- Vital signs link
|
|
- Assessment and recommendations
|
|
- Family satisfaction score
|
|
- Medication snapshot
|
|
"""
|
|
model = MedicalFollowUp
|
|
template_name = 'medical/followup_detail.html'
|
|
context_object_name = 'followup'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add related data."""
|
|
context = super().get_context_data(**kwargs)
|
|
followup = self.object
|
|
|
|
# Get previous consultation
|
|
if followup.previous_consultation:
|
|
context['previous_consultation'] = followup.previous_consultation
|
|
# medications is a JSONField, not a related manager
|
|
context['previous_medications'] = followup.previous_consultation.medications if followup.previous_consultation.medications else []
|
|
|
|
# Get vital signs from nursing
|
|
from nursing.models import NursingEncounter
|
|
if followup.appointment:
|
|
context['vitals'] = NursingEncounter.objects.filter(
|
|
appointment=followup.appointment
|
|
).first()
|
|
|
|
return context
|
|
|
|
|
|
class MedicalFollowUpCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
|
|
AuditLogMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
Medical follow-up creation view (MD-F-2).
|
|
|
|
Features:
|
|
- Link to previous consultation
|
|
- Previous complaints with status (Resolved/Static/Worse)
|
|
- New complaints
|
|
- Vital signs reference
|
|
- Assessment and recommendations
|
|
- Family satisfaction (0/50/100)
|
|
- Medication table snapshot
|
|
- Consent verification enforced before creation
|
|
"""
|
|
model = MedicalFollowUp
|
|
form_class = MedicalFollowUpForm
|
|
template_name = 'medical/followup_form.html'
|
|
success_message = _("Medical follow-up recorded successfully!")
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
|
|
|
|
# Consent enforcement
|
|
consent_service_type = 'MEDICAL'
|
|
consent_error_message = _(
|
|
"Patient must sign general treatment consent before medical follow-up can be documented."
|
|
)
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to follow-up detail."""
|
|
return reverse_lazy('medical:followup_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def get_patient(self):
|
|
"""Get patient for consent verification."""
|
|
patient_id = self.request.GET.get('patient')
|
|
appointment_id = self.request.GET.get('appointment_id')
|
|
|
|
if patient_id:
|
|
return Patient.objects.get(
|
|
pk=patient_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
elif appointment_id:
|
|
appointment = Appointment.objects.get(
|
|
pk=appointment_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
return appointment.patient
|
|
|
|
return None
|
|
|
|
def get_initial(self):
|
|
"""Pre-populate form fields from appointment."""
|
|
initial = super().get_initial()
|
|
|
|
appointment_id = self.request.GET.get('appointment_id')
|
|
if appointment_id:
|
|
try:
|
|
appointment = Appointment.objects.get(pk=appointment_id, tenant=self.request.user.tenant)
|
|
initial['patient'] = appointment.patient.pk
|
|
initial['appointment'] = appointment.pk
|
|
initial['followup_date'] = appointment.appointment_date
|
|
initial['provider'] = self.request.user.pk
|
|
except Appointment.DoesNotExist:
|
|
pass
|
|
|
|
patient_id = self.request.GET.get('patient')
|
|
if patient_id and not appointment_id:
|
|
initial['patient'] = patient_id
|
|
initial['provider'] = self.request.user.pk
|
|
initial['followup_date'] = timezone.now().date()
|
|
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
"""Set tenant and provider."""
|
|
# Set tenant
|
|
form.instance.tenant = self.request.user.tenant
|
|
|
|
# Set provider if not set
|
|
if not form.instance.provider:
|
|
form.instance.provider = self.request.user
|
|
|
|
# Set follow-up date if not provided
|
|
if not form.instance.followup_date:
|
|
form.instance.followup_date = timezone.now().date()
|
|
|
|
# Save follow-up
|
|
response = super().form_valid(form)
|
|
|
|
return response
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add form title and previous consultation info."""
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_title'] = _('Medical Follow-up (MD-F-2)')
|
|
context['submit_text'] = _('Save Follow-up')
|
|
|
|
# Get patient if provided
|
|
patient_id = self.request.GET.get('patient')
|
|
if patient_id:
|
|
try:
|
|
patient = Patient.objects.get(
|
|
pk=patient_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
context['patient'] = patient
|
|
|
|
# Get most recent consultation for this patient
|
|
context['recent_consultations'] = MedicalConsultation.objects.filter(
|
|
patient=patient
|
|
).order_by('-consultation_date')[:5]
|
|
|
|
except Patient.DoesNotExist:
|
|
pass
|
|
|
|
# Get appointment if provided
|
|
appointment_id = self.request.GET.get('appointment_id')
|
|
if appointment_id:
|
|
try:
|
|
context['appointment'] = Appointment.objects.get(
|
|
pk=appointment_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
context['patient'] = context['appointment'].patient
|
|
|
|
# Get most recent consultations for this patient
|
|
context['recent_consultations'] = MedicalConsultation.objects.filter(
|
|
patient=context['appointment'].patient
|
|
).order_by('-consultation_date')[:5]
|
|
except Appointment.DoesNotExist:
|
|
pass
|
|
|
|
return context
|
|
|
|
|
|
class MedicalFollowUpUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
|
|
AuditLogMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Medical follow-up update view (MD-F-2).
|
|
|
|
Features:
|
|
- Update follow-up details
|
|
- Version history
|
|
- Audit trail
|
|
"""
|
|
model = MedicalFollowUp
|
|
form_class = MedicalFollowUpForm
|
|
template_name = 'medical/followup_form.html'
|
|
success_message = _("Medical follow-up updated successfully!")
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to follow-up detail."""
|
|
return reverse_lazy('medical:followup_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add form title and history."""
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_title'] = _('Update Medical Follow-up - %(mrn)s') % {'mrn': self.object.patient.mrn}
|
|
context['submit_text'] = _('Update Follow-up')
|
|
|
|
# Add version history if available
|
|
if hasattr(self.object, 'history'):
|
|
context['history'] = self.object.history.all()[:10]
|
|
|
|
return context
|
|
|
|
|
|
class ConsultationResponseCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin,
|
|
SuccessMessageMixin, CreateView):
|
|
"""
|
|
Create response to medical consultation from other disciplines.
|
|
|
|
Features:
|
|
- OT, SLP, ABA, Nursing can respond to consultations
|
|
- Assessment and recommendations
|
|
- Follow-up needed flag
|
|
"""
|
|
model = ConsultationResponse
|
|
form_class = ConsultationResponseForm
|
|
template_name = 'medical/response_form.html'
|
|
success_message = _("Response submitted successfully!")
|
|
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR, User.Role.NURSE,
|
|
User.Role.OT, User.Role.SLP, User.Role.ABA]
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to consultation detail."""
|
|
return reverse_lazy('medical:consultation_detail',
|
|
kwargs={'pk': self.object.consultation.pk})
|
|
|
|
def get_initial(self):
|
|
"""Pre-populate consultation and responder."""
|
|
initial = super().get_initial()
|
|
|
|
consultation_id = self.kwargs.get('consultation_pk')
|
|
if consultation_id:
|
|
initial['consultation'] = consultation_id
|
|
initial['responder'] = self.request.user.pk
|
|
initial['response_date'] = timezone.now().date()
|
|
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
"""Set tenant and responder."""
|
|
form.instance.tenant = self.request.user.tenant
|
|
form.instance.responder = self.request.user
|
|
|
|
if not form.instance.response_date:
|
|
form.instance.response_date = timezone.now().date()
|
|
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add consultation info."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
consultation_id = self.kwargs.get('consultation_pk')
|
|
if consultation_id:
|
|
try:
|
|
consultation = MedicalConsultation.objects.get(
|
|
pk=consultation_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
context['consultation'] = consultation
|
|
context['form_title'] = _('Respond to Consultation - %(mrn)s') % {'mrn': consultation.patient.mrn}
|
|
except MedicalConsultation.DoesNotExist:
|
|
pass
|
|
|
|
return context
|
|
|
|
|
|
class ConsultationFeedbackCreateView(LoginRequiredMixin, AuditLogMixin,
|
|
SuccessMessageMixin, CreateView):
|
|
"""
|
|
Create feedback for medical consultation.
|
|
|
|
Features:
|
|
- Family/caregiver feedback
|
|
- Team feedback
|
|
- Ratings and comments
|
|
"""
|
|
model = ConsultationFeedback
|
|
form_class = ConsultationFeedbackForm
|
|
template_name = 'medical/feedback_form.html'
|
|
success_message = _("Feedback submitted successfully!")
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to consultation detail."""
|
|
return reverse_lazy('medical:consultation_detail',
|
|
kwargs={'pk': self.object.consultation.pk})
|
|
|
|
def get_initial(self):
|
|
"""Pre-populate consultation."""
|
|
initial = super().get_initial()
|
|
|
|
consultation_id = self.kwargs.get('consultation_pk')
|
|
if consultation_id:
|
|
initial['consultation'] = consultation_id
|
|
initial['feedback_date'] = timezone.now().date()
|
|
|
|
# Set submitted_by if user is logged in
|
|
if self.request.user.is_authenticated:
|
|
initial['submitted_by'] = self.request.user.pk
|
|
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
"""Set submitted_by if authenticated."""
|
|
if self.request.user.is_authenticated and not form.instance.submitted_by:
|
|
form.instance.submitted_by = self.request.user
|
|
|
|
if not form.instance.feedback_date:
|
|
form.instance.feedback_date = timezone.now().date()
|
|
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add consultation info."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
consultation_id = self.kwargs.get('consultation_pk')
|
|
if consultation_id:
|
|
try:
|
|
consultation = MedicalConsultation.objects.get(pk=consultation_id)
|
|
context['consultation'] = consultation
|
|
context['form_title'] = _('Provide Feedback - %(mrn)s') % {'mrn': consultation.patient.mrn}
|
|
except MedicalConsultation.DoesNotExist:
|
|
pass
|
|
|
|
return context
|
|
|
|
|
|
# ============================================================================
|
|
# PDF Generation Views
|
|
# ============================================================================
|
|
|
|
from core.pdf_service import BasePDFGenerator
|
|
|
|
|
|
class MedicalConsultationPDFGenerator(BasePDFGenerator):
|
|
"""PDF generator for Medical Consultation (MD-F-1)."""
|
|
|
|
def get_document_title(self):
|
|
"""Return document title in English and Arabic."""
|
|
consultation = self.document
|
|
return (
|
|
f"Medical Consultation (MD-F-1) - {consultation.patient.mrn}",
|
|
"استشارة طبية"
|
|
)
|
|
|
|
def get_pdf_filename(self):
|
|
"""Return PDF filename."""
|
|
consultation = self.document
|
|
date_str = consultation.consultation_date.strftime('%Y%m%d')
|
|
return f"medical_consultation_{consultation.patient.mrn}_{date_str}.pdf"
|
|
|
|
def get_document_sections(self):
|
|
"""Return document sections to render."""
|
|
consultation = self.document
|
|
patient = consultation.patient
|
|
|
|
sections = []
|
|
|
|
# Patient Information Section
|
|
patient_name_ar = f"{patient.first_name_ar} {patient.last_name_ar}" if patient.first_name_ar else ""
|
|
sections.append({
|
|
'heading_en': 'Patient Information',
|
|
'heading_ar': 'معلومات المريض',
|
|
'type': 'table',
|
|
'content': [
|
|
('Name', 'الاسم', f"{patient.first_name_en} {patient.last_name_en}", patient_name_ar),
|
|
('MRN', 'رقم السجل الطبي', patient.mrn, ""),
|
|
('Date of Birth', 'تاريخ الميلاد', patient.date_of_birth.strftime('%Y-%m-%d'), ""),
|
|
('Age', 'العمر', f"{patient.age} years", ""),
|
|
('Gender', 'الجنس', patient.get_sex_display(), ""),
|
|
]
|
|
})
|
|
|
|
# Consultation Details Section
|
|
sections.append({
|
|
'heading_en': 'Consultation Details',
|
|
'heading_ar': 'تفاصيل الاستشارة',
|
|
'type': 'table',
|
|
'content': [
|
|
('Date', 'التاريخ', consultation.consultation_date.strftime('%Y-%m-%d'), ""),
|
|
('Provider', 'مقدم الخدمة', consultation.provider.get_full_name() if consultation.provider else 'N/A', ""),
|
|
('Signed By', 'موقع من قبل', consultation.signed_by.get_full_name() if consultation.signed_by else 'Not signed', ""),
|
|
('Signed At', 'تاريخ التوقيع', consultation.signed_at.strftime('%Y-%m-%d %H:%M') if consultation.signed_at else 'N/A', ""),
|
|
]
|
|
})
|
|
|
|
# Chief Complaint & History
|
|
if consultation.chief_complaint:
|
|
sections.append({
|
|
'heading_en': 'Chief Complaint',
|
|
'heading_ar': 'الشكوى الرئيسية',
|
|
'type': 'text',
|
|
'content': [consultation.chief_complaint]
|
|
})
|
|
|
|
if consultation.present_illness_history:
|
|
sections.append({
|
|
'heading_en': 'History of Present Illness',
|
|
'heading_ar': 'تاريخ المرض الحالي',
|
|
'type': 'text',
|
|
'content': [consultation.present_illness_history]
|
|
})
|
|
|
|
if consultation.past_medical_history:
|
|
sections.append({
|
|
'heading_en': 'Past Medical History',
|
|
'heading_ar': 'التاريخ الطبي السابق',
|
|
'type': 'text',
|
|
'content': [consultation.past_medical_history]
|
|
})
|
|
|
|
# Developmental Milestones
|
|
if any([consultation.developmental_motor_milestones, consultation.developmental_language_milestones,
|
|
consultation.developmental_social_milestones, consultation.developmental_cognitive_milestones]):
|
|
dev_content = []
|
|
if consultation.developmental_motor_milestones:
|
|
dev_content.append(f"<b>Motor:</b> {consultation.developmental_motor_milestones}")
|
|
if consultation.developmental_language_milestones:
|
|
dev_content.append(f"<b>Language:</b> {consultation.developmental_language_milestones}")
|
|
if consultation.developmental_social_milestones:
|
|
dev_content.append(f"<b>Social:</b> {consultation.developmental_social_milestones}")
|
|
if consultation.developmental_cognitive_milestones:
|
|
dev_content.append(f"<b>Cognitive:</b> {consultation.developmental_cognitive_milestones}")
|
|
|
|
sections.append({
|
|
'heading_en': 'Developmental Milestones',
|
|
'heading_ar': 'المعالم التنموية',
|
|
'type': 'text',
|
|
'content': dev_content
|
|
})
|
|
|
|
# Clinical Summary & Recommendations
|
|
if consultation.clinical_summary:
|
|
sections.append({
|
|
'heading_en': 'Clinical Summary',
|
|
'heading_ar': 'الملخص السريري',
|
|
'type': 'text',
|
|
'content': [consultation.clinical_summary]
|
|
})
|
|
|
|
if consultation.recommendations:
|
|
sections.append({
|
|
'heading_en': 'Recommendations',
|
|
'heading_ar': 'التوصيات',
|
|
'type': 'text',
|
|
'content': [consultation.recommendations]
|
|
})
|
|
|
|
# Medications
|
|
if consultation.medications:
|
|
med_content = []
|
|
for med in consultation.medications:
|
|
med_text = f"• <b>{med.get('drug_name', 'N/A')}</b> - {med.get('dose', 'N/A')} - {med.get('frequency', 'N/A')}"
|
|
if med.get('compliance'):
|
|
med_text += f" (Compliance: {med.get('compliance')})"
|
|
med_content.append(med_text)
|
|
|
|
sections.append({
|
|
'heading_en': 'Current Medications',
|
|
'heading_ar': 'الأدوية الحالية',
|
|
'type': 'text',
|
|
'content': med_content
|
|
})
|
|
|
|
return sections
|
|
|
|
|
|
class MedicalConsultationPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Generate PDF for medical consultation."""
|
|
|
|
def get(self, request, pk):
|
|
"""Generate and return PDF."""
|
|
consultation = get_object_or_404(
|
|
MedicalConsultation.objects.select_related(
|
|
'patient', 'provider', 'tenant', 'signed_by'
|
|
),
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
pdf_generator = MedicalConsultationPDFGenerator(consultation, request)
|
|
view_mode = request.GET.get('view', 'download')
|
|
return pdf_generator.generate_pdf(view_mode=view_mode)
|
|
|
|
|
|
class MedicalConsultationEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Email medical consultation PDF to patient."""
|
|
|
|
def post(self, request, pk):
|
|
"""Send PDF via email."""
|
|
consultation = get_object_or_404(
|
|
MedicalConsultation.objects.select_related(
|
|
'patient', 'provider', 'tenant'
|
|
),
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
email_address = request.POST.get('email_address', '').strip()
|
|
custom_message = request.POST.get('email_message', '').strip()
|
|
|
|
if not email_address:
|
|
messages.error(request, _('Email address is required.'))
|
|
return redirect('medical:consultation_detail', pk=pk)
|
|
|
|
pdf_generator = MedicalConsultationPDFGenerator(consultation, request)
|
|
|
|
subject = f"Medical Consultation - {consultation.patient.mrn}"
|
|
body = f"""
|
|
Dear {consultation.patient.first_name_en} {consultation.patient.last_name_en},
|
|
|
|
Please find attached your medical consultation details.
|
|
|
|
Consultation Date: {consultation.consultation_date.strftime('%Y-%m-%d')}
|
|
Provider: {consultation.provider.get_full_name() if consultation.provider else 'N/A'}
|
|
|
|
Best regards,
|
|
{consultation.tenant.name}
|
|
"""
|
|
|
|
success, message = pdf_generator.send_email(
|
|
email_address=email_address,
|
|
subject=subject,
|
|
body=body,
|
|
custom_message=custom_message
|
|
)
|
|
|
|
if success:
|
|
messages.success(request, _('PDF sent to %(email)s successfully!') % {'email': email_address})
|
|
else:
|
|
messages.error(request, _('Failed to send email: %(error)s') % {'error': message})
|
|
|
|
return redirect('medical:consultation_detail', pk=pk)
|
|
|
|
|
|
class MedicalFollowUpPDFGenerator(BasePDFGenerator):
|
|
"""PDF generator for Medical Follow-up (MD-F-2)."""
|
|
|
|
def get_document_title(self):
|
|
"""Return document title in English and Arabic."""
|
|
followup = self.document
|
|
return (
|
|
f"Medical Follow-up (MD-F-2) - {followup.patient.mrn}",
|
|
"متابعة طبية"
|
|
)
|
|
|
|
def get_pdf_filename(self):
|
|
"""Return PDF filename."""
|
|
followup = self.document
|
|
date_str = followup.followup_date.strftime('%Y%m%d')
|
|
return f"medical_followup_{followup.patient.mrn}_{date_str}.pdf"
|
|
|
|
def get_document_sections(self):
|
|
"""Return document sections to render."""
|
|
followup = self.document
|
|
patient = followup.patient
|
|
|
|
sections = []
|
|
|
|
# Patient Information Section
|
|
patient_name_ar = f"{patient.first_name_ar} {patient.last_name_ar}" if patient.first_name_ar else ""
|
|
sections.append({
|
|
'heading_en': 'Patient Information',
|
|
'heading_ar': 'معلومات المريض',
|
|
'type': 'table',
|
|
'content': [
|
|
('Name', 'الاسم', f"{patient.first_name_en} {patient.last_name_en}", patient_name_ar),
|
|
('MRN', 'رقم السجل الطبي', patient.mrn, ""),
|
|
('Date of Birth', 'تاريخ الميلاد', patient.date_of_birth.strftime('%Y-%m-%d'), ""),
|
|
('Age', 'العمر', f"{patient.age} years", ""),
|
|
]
|
|
})
|
|
|
|
# Follow-up Details Section
|
|
sections.append({
|
|
'heading_en': 'Follow-up Details',
|
|
'heading_ar': 'تفاصيل المتابعة',
|
|
'type': 'table',
|
|
'content': [
|
|
('Date', 'التاريخ', followup.followup_date.strftime('%Y-%m-%d'), ""),
|
|
('Provider', 'مقدم الخدمة', followup.provider.get_full_name() if followup.provider else 'N/A', ""),
|
|
('Family Satisfaction', 'رضا العائلة', followup.get_family_satisfaction_display() if followup.family_satisfaction else 'N/A', ""),
|
|
('Signed By', 'موقع من قبل', followup.signed_by.get_full_name() if followup.signed_by else 'Not signed', ""),
|
|
]
|
|
})
|
|
|
|
# New Complaints
|
|
if followup.new_complaints:
|
|
sections.append({
|
|
'heading_en': 'New Complaints',
|
|
'heading_ar': 'الشكاوى الجديدة',
|
|
'type': 'text',
|
|
'content': [followup.new_complaints]
|
|
})
|
|
|
|
# Assessment & Recommendations
|
|
if followup.assessment:
|
|
sections.append({
|
|
'heading_en': 'Assessment',
|
|
'heading_ar': 'التقييم',
|
|
'type': 'text',
|
|
'content': [followup.assessment]
|
|
})
|
|
|
|
if followup.recommendations:
|
|
sections.append({
|
|
'heading_en': 'Recommendations',
|
|
'heading_ar': 'التوصيات',
|
|
'type': 'text',
|
|
'content': [followup.recommendations]
|
|
})
|
|
|
|
# Medication Snapshot
|
|
if followup.medication_snapshot:
|
|
med_content = []
|
|
for med in followup.medication_snapshot:
|
|
med_text = f"• <b>{med.get('drug_name', 'N/A')}</b> - {med.get('dose', 'N/A')} - {med.get('frequency', 'N/A')}"
|
|
if med.get('compliance'):
|
|
med_text += f" (Compliance: {med.get('compliance')})"
|
|
if med.get('improved'):
|
|
med_text += " ✓ Improved"
|
|
med_content.append(med_text)
|
|
|
|
sections.append({
|
|
'heading_en': 'Current Medications',
|
|
'heading_ar': 'الأدوية الحالية',
|
|
'type': 'text',
|
|
'content': med_content
|
|
})
|
|
|
|
return sections
|
|
|
|
|
|
class MedicalFollowUpPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Generate PDF for medical follow-up."""
|
|
|
|
def get(self, request, pk):
|
|
"""Generate and return PDF."""
|
|
followup = get_object_or_404(
|
|
MedicalFollowUp.objects.select_related(
|
|
'patient', 'provider', 'tenant', 'signed_by'
|
|
),
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
pdf_generator = MedicalFollowUpPDFGenerator(followup, request)
|
|
view_mode = request.GET.get('view', 'download')
|
|
return pdf_generator.generate_pdf(view_mode=view_mode)
|
|
|
|
|
|
class MedicalFollowUpEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Email medical follow-up PDF to patient."""
|
|
|
|
def post(self, request, pk):
|
|
"""Send PDF via email."""
|
|
followup = get_object_or_404(
|
|
MedicalFollowUp.objects.select_related(
|
|
'patient', 'provider', 'tenant'
|
|
),
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
email_address = request.POST.get('email_address', '').strip()
|
|
custom_message = request.POST.get('email_message', '').strip()
|
|
|
|
if not email_address:
|
|
messages.error(request, _('Email address is required.'))
|
|
return redirect('medical:followup_detail', pk=pk)
|
|
|
|
pdf_generator = MedicalFollowUpPDFGenerator(followup, request)
|
|
|
|
subject = f"Medical Follow-up - {followup.patient.mrn}"
|
|
body = f"""
|
|
Dear {followup.patient.first_name_en} {followup.patient.last_name_en},
|
|
|
|
Please find attached your medical follow-up details.
|
|
|
|
Follow-up Date: {followup.followup_date.strftime('%Y-%m-%d')}
|
|
Provider: {followup.provider.get_full_name() if followup.provider else 'N/A'}
|
|
|
|
Best regards,
|
|
{followup.tenant.name}
|
|
"""
|
|
|
|
success, message = pdf_generator.send_email(
|
|
email_address=email_address,
|
|
subject=subject,
|
|
body=body,
|
|
custom_message=custom_message
|
|
)
|
|
|
|
if success:
|
|
messages.success(request, _('PDF sent to %(email)s successfully!') % {'email': email_address})
|
|
else:
|
|
messages.error(request, _('Failed to send email: %(error)s') % {'error': message})
|
|
|
|
return redirect('medical:followup_detail', pk=pk)
|