1176 lines
41 KiB
Python
1176 lines
41 KiB
Python
"""
|
|
OT views for the Tenhal Multidisciplinary Healthcare Platform.
|
|
|
|
This module contains views for Occupational Therapy documentation including:
|
|
- OT consultations (OT-F-1)
|
|
- OT session notes (OT-F-3)
|
|
- Target skills tracking with 0-10 scoring
|
|
- Progress visualization
|
|
"""
|
|
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.contrib import messages
|
|
from django.db.models import Q, Avg
|
|
from django.http import HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils import timezone
|
|
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
|
|
from django.urls import reverse_lazy
|
|
|
|
from core.mixins import (
|
|
TenantFilterMixin,
|
|
RolePermissionMixin,
|
|
AuditLogMixin,
|
|
HTMXResponseMixin,
|
|
SuccessMessageMixin,
|
|
PaginationMixin,
|
|
ConsentRequiredMixin,
|
|
)
|
|
from core.models import User, Patient
|
|
from appointments.models import Appointment
|
|
from .models import OTConsult, OTSession, OTTargetSkill
|
|
from .forms import OTConsultForm, OTSessionForm
|
|
|
|
|
|
class OTConsultSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
|
|
"""
|
|
Sign an OT consultation.
|
|
|
|
Features:
|
|
- Only the provider or admin can sign
|
|
- Records signature timestamp and user
|
|
- Prevents re-signing already signed consultations
|
|
"""
|
|
allowed_roles = [User.Role.ADMIN, User.Role.OT]
|
|
|
|
def post(self, request, pk):
|
|
"""Sign the consultation."""
|
|
consult = get_object_or_404(
|
|
OTConsult,
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
# Check if already signed
|
|
if consult.signed_by:
|
|
messages.warning(
|
|
request,
|
|
"This consultation has already been signed."
|
|
)
|
|
return HttpResponseRedirect(
|
|
reverse_lazy('ot:consult_detail', kwargs={'pk': pk})
|
|
)
|
|
|
|
# Check if user is the provider or admin
|
|
if consult.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('ot:consult_detail', kwargs={'pk': pk})
|
|
)
|
|
|
|
# Sign the consultation
|
|
consult.signed_by = request.user
|
|
consult.signed_at = timezone.now()
|
|
consult.save()
|
|
|
|
messages.success(
|
|
request,
|
|
"Consultation signed successfully!"
|
|
)
|
|
|
|
return HttpResponseRedirect(
|
|
reverse_lazy('ot:consult_detail', kwargs={'pk': pk})
|
|
)
|
|
|
|
|
|
class OTSessionSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
|
|
"""
|
|
Sign an OT session.
|
|
|
|
Features:
|
|
- Only the provider or admin can sign
|
|
- Records signature timestamp and user
|
|
- Prevents re-signing already signed sessions
|
|
"""
|
|
allowed_roles = [User.Role.ADMIN, User.Role.OT]
|
|
|
|
def post(self, request, pk):
|
|
"""Sign the session."""
|
|
session = get_object_or_404(
|
|
OTSession,
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
# Check if already signed
|
|
if session.signed_by:
|
|
messages.warning(
|
|
request,
|
|
"This session has already been signed."
|
|
)
|
|
return HttpResponseRedirect(
|
|
reverse_lazy('ot:session_detail', kwargs={'pk': pk})
|
|
)
|
|
|
|
# Check if user is the provider or admin
|
|
if session.provider != request.user and request.user.role != User.Role.ADMIN:
|
|
messages.error(
|
|
request,
|
|
"Only the session provider or an administrator can sign this session."
|
|
)
|
|
return HttpResponseRedirect(
|
|
reverse_lazy('ot:session_detail', kwargs={'pk': pk})
|
|
)
|
|
|
|
# Sign the session
|
|
session.signed_by = request.user
|
|
session.signed_at = timezone.now()
|
|
session.save()
|
|
|
|
messages.success(
|
|
request,
|
|
"Session signed successfully!"
|
|
)
|
|
|
|
return HttpResponseRedirect(
|
|
reverse_lazy('ot:session_detail', kwargs={'pk': pk})
|
|
)
|
|
|
|
|
|
class OTConsultListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
|
|
HTMXResponseMixin, ListView):
|
|
"""
|
|
OT consultation list view (OT-F-1).
|
|
|
|
Features:
|
|
- Filter by patient, provider, date range
|
|
- Search by patient name/MRN
|
|
- Role-based filtering
|
|
- Shows unsigned consultations notification
|
|
"""
|
|
model = OTConsult
|
|
template_name = 'ot/consult_list.html'
|
|
htmx_template_name = 'ot/partials/consult_list_partial.html'
|
|
context_object_name = 'consults'
|
|
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.OT:
|
|
queryset = queryset.filter(provider=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(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 unsigned consultations count for current user."""
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
|
|
# Get unsigned consultations for current user
|
|
unsigned_query = OTConsult.objects.filter(
|
|
tenant=user.tenant,
|
|
signed_by__isnull=True
|
|
)
|
|
|
|
# Filter by provider if OT role
|
|
if user.role == User.Role.OT:
|
|
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]
|
|
|
|
return context
|
|
|
|
|
|
class OTConsultDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
|
|
"""
|
|
OT consultation detail view (OT-F-1).
|
|
|
|
Features:
|
|
- Full consultation details
|
|
- Reasons for referral (multi-select)
|
|
- Top 3 difficulty areas
|
|
- Developmental motor milestones
|
|
- Self-help skills matrix
|
|
- Feeding participation
|
|
- Infant/now behavior descriptors
|
|
- Recommendation (continue/discharge/referral with rules)
|
|
"""
|
|
model = OTConsult
|
|
template_name = 'ot/consult_detail.html'
|
|
context_object_name = 'consult'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add related sessions."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Get related sessions
|
|
context['sessions'] = OTSession.objects.filter(
|
|
patient=self.object.patient
|
|
).order_by('-session_date')[:10]
|
|
|
|
return context
|
|
|
|
|
|
class OTConsultCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
|
|
AuditLogMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
OT consultation creation view (OT-F-1).
|
|
|
|
Features:
|
|
- Reasons for referral (multi-select)
|
|
- Top 3 difficulty areas (text)
|
|
- Developmental motor milestones
|
|
- Self-help skills matrix
|
|
- Feeding participation
|
|
- Infant/now behavior descriptors
|
|
- Recommendation (continue/discharge/referral with rules)
|
|
- Consent verification enforced before creation
|
|
"""
|
|
model = OTConsult
|
|
form_class = OTConsultForm
|
|
template_name = 'ot/consult_form.html'
|
|
success_message = "OT consultation recorded successfully!"
|
|
allowed_roles = [User.Role.ADMIN, User.Role.OT]
|
|
|
|
# Consent enforcement
|
|
consent_service_type = 'OT'
|
|
consent_error_message = (
|
|
"Patient must sign OT therapy consent before consultation can be documented."
|
|
)
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to consult detail."""
|
|
return reverse_lazy('ot:consult_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."""
|
|
# 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()
|
|
|
|
# Save consultation
|
|
response = super().form_valid(form)
|
|
|
|
return response
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add form title and patient/appointment info."""
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_title'] = 'OT Consultation (OT-F-1)'
|
|
context['submit_text'] = 'Save Consultation'
|
|
|
|
# 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 OTConsultUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
|
|
AuditLogMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
OT consultation update view (OT-F-1).
|
|
|
|
Features:
|
|
- Update consultation details
|
|
- Version history
|
|
- Audit trail
|
|
"""
|
|
model = OTConsult
|
|
form_class = OTConsultForm
|
|
template_name = 'ot/consult_form.html'
|
|
success_message = "OT consultation updated successfully!"
|
|
allowed_roles = [User.Role.ADMIN, User.Role.OT]
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to consult detail."""
|
|
return reverse_lazy('ot:consult_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'] = f'Update OT Consultation - {self.object.patient.mrn}'
|
|
context['submit_text'] = 'Update Consultation'
|
|
|
|
# Add version history if available
|
|
if hasattr(self.object, 'history'):
|
|
context['history'] = self.object.history.all()[:10]
|
|
|
|
return context
|
|
|
|
|
|
class OTSessionListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
|
|
HTMXResponseMixin, ListView):
|
|
"""
|
|
OT session list view (OT-F-3).
|
|
|
|
Features:
|
|
- Filter by patient, provider, date range, session type
|
|
- Search by patient name/MRN
|
|
- Role-based filtering
|
|
- Shows unsigned sessions notification
|
|
"""
|
|
model = OTSession
|
|
template_name = 'ot/session_list.html'
|
|
htmx_template_name = 'ot/partials/session_list_partial.html'
|
|
context_object_name = 'sessions'
|
|
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.OT:
|
|
queryset = queryset.filter(provider=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)
|
|
|
|
session_type = self.request.GET.get('session_type')
|
|
if session_type:
|
|
queryset = queryset.filter(session_type=session_type)
|
|
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(session_date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(session_date__lte=date_to)
|
|
|
|
return queryset.select_related('patient', 'provider', 'appointment', 'signed_by').order_by('-session_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add unsigned sessions count for current user."""
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
|
|
# Get unsigned sessions for current user
|
|
unsigned_query = OTSession.objects.filter(
|
|
tenant=user.tenant,
|
|
signed_by__isnull=True
|
|
)
|
|
|
|
# Filter by provider if OT role
|
|
if user.role == User.Role.OT:
|
|
unsigned_query = unsigned_query.filter(provider=user)
|
|
|
|
context['unsigned_count'] = unsigned_query.count()
|
|
context['unsigned_items'] = unsigned_query.select_related(
|
|
'patient', 'provider'
|
|
).order_by('-session_date')[:10]
|
|
|
|
return context
|
|
|
|
|
|
class OTSessionDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
|
|
"""
|
|
OT session detail view (OT-F-3).
|
|
|
|
Features:
|
|
- Full session details
|
|
- Session type (Consult/Individual/Group/ParentTraining)
|
|
- Cooperative level (1-4)
|
|
- Distraction tolerance (1-4)
|
|
- "Today we work on..." checklist
|
|
- Target skills with 0-10 scoring
|
|
- Observations, activities, recommendations
|
|
"""
|
|
model = OTSession
|
|
template_name = 'ot/session_detail.html'
|
|
context_object_name = 'session'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add target skills and progress data."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Get target skills
|
|
context['target_skills'] = self.object.target_skills.all()
|
|
|
|
# Get previous sessions for comparison
|
|
context['previous_sessions'] = OTSession.objects.filter(
|
|
patient=self.object.patient,
|
|
session_date__lt=self.object.session_date
|
|
).order_by('-session_date')[:5]
|
|
|
|
return context
|
|
|
|
|
|
class OTSessionCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
|
|
AuditLogMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
OT session creation view (OT-F-3).
|
|
|
|
Features:
|
|
- Session date, type (Consult/Individual/Group/ParentTraining)
|
|
- Cooperative level (1-4)
|
|
- Distraction tolerance (1-4)
|
|
- "Today we work on..." checklist
|
|
- Target skills with 0-10 scoring
|
|
- Observations, activities, recommendations
|
|
- Consent verification enforced before creation
|
|
"""
|
|
model = OTSession
|
|
form_class = OTSessionForm
|
|
template_name = 'ot/session_form.html'
|
|
success_message = "OT session recorded successfully!"
|
|
allowed_roles = [User.Role.ADMIN, User.Role.OT]
|
|
|
|
# Consent enforcement
|
|
consent_service_type = 'OT'
|
|
consent_error_message = (
|
|
"Patient must sign OT therapy consent before session can be documented."
|
|
)
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to session detail."""
|
|
return reverse_lazy('ot:session_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."""
|
|
# Set tenant
|
|
form.instance.tenant = self.request.user.tenant
|
|
|
|
# Set provider
|
|
form.instance.provider = self.request.user
|
|
|
|
# Set session date if not provided
|
|
if not form.instance.session_date:
|
|
form.instance.session_date = timezone.now().date()
|
|
|
|
# Save session
|
|
response = super().form_valid(form)
|
|
|
|
return response
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add form title and patient/appointment info."""
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_title'] = 'OT Session Note (OT-F-3)'
|
|
context['submit_text'] = 'Save Session'
|
|
|
|
# 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 OTSessionUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
|
|
AuditLogMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
OT session update view (OT-F-3).
|
|
|
|
Features:
|
|
- Update session details
|
|
- Version history
|
|
- Audit trail
|
|
"""
|
|
model = OTSession
|
|
form_class = OTSessionForm
|
|
template_name = 'ot/session_form.html'
|
|
success_message = "OT session updated successfully!"
|
|
allowed_roles = [User.Role.ADMIN, User.Role.OT]
|
|
|
|
def get_success_url(self):
|
|
"""Redirect to session detail."""
|
|
return reverse_lazy('ot:session_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'] = f'Update OT Session - {self.object.patient.mrn}'
|
|
context['submit_text'] = 'Update Session'
|
|
|
|
# Add version history if available
|
|
if hasattr(self.object, 'history'):
|
|
context['history'] = self.object.history.all()[:10]
|
|
|
|
return context
|
|
|
|
|
|
class PatientOTProgressView(LoginRequiredMixin, TenantFilterMixin, ListView):
|
|
"""
|
|
Patient OT progress view.
|
|
|
|
Features:
|
|
- All OT sessions for specific patient
|
|
- Target skills progress tracking (0-10 scoring graph)
|
|
- Cooperative level trends
|
|
- Distraction tolerance trends
|
|
"""
|
|
model = OTSession
|
|
template_name = 'ot/patient_progress.html'
|
|
context_object_name = 'sessions'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
"""Get all sessions for patient."""
|
|
patient_id = self.kwargs.get('patient_id')
|
|
return OTSession.objects.filter(
|
|
patient_id=patient_id,
|
|
tenant=self.request.user.tenant
|
|
).order_by('-session_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add patient and progress data."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
patient_id = self.kwargs.get('patient_id')
|
|
context['patient'] = get_object_or_404(
|
|
Patient,
|
|
pk=patient_id,
|
|
tenant=self.request.user.tenant
|
|
)
|
|
|
|
# Get progress data for charts
|
|
sessions = self.get_queryset()
|
|
context['progress_data'] = self._prepare_progress_data(sessions)
|
|
|
|
# Get statistics
|
|
context['stats'] = self._calculate_statistics(sessions)
|
|
|
|
return context
|
|
|
|
def _prepare_progress_data(self, sessions):
|
|
"""Prepare data for progress charts."""
|
|
data = {
|
|
'dates': [],
|
|
'cooperative_level': [],
|
|
'distraction_tolerance': [],
|
|
'target_skills': {},
|
|
}
|
|
|
|
for session in sessions.order_by('session_date'):
|
|
data['dates'].append(session.session_date.strftime('%Y-%m-%d'))
|
|
data['cooperative_level'].append(session.cooperative_level)
|
|
data['distraction_tolerance'].append(session.distraction_tolerance)
|
|
|
|
# Track target skills
|
|
for skill in session.target_skills.all():
|
|
if skill.skill_name not in data['target_skills']:
|
|
data['target_skills'][skill.skill_name] = []
|
|
data['target_skills'][skill.skill_name].append(skill.score)
|
|
|
|
return data
|
|
|
|
def _calculate_statistics(self, sessions):
|
|
"""Calculate statistics from sessions."""
|
|
if not sessions:
|
|
return {}
|
|
|
|
return {
|
|
'total_sessions': sessions.count(),
|
|
'avg_cooperative_level': sessions.aggregate(Avg('cooperative_level'))['cooperative_level__avg'],
|
|
'avg_distraction_tolerance': sessions.aggregate(Avg('distraction_tolerance'))['distraction_tolerance__avg'],
|
|
'latest_session': sessions.first(),
|
|
}
|
|
|
|
|
|
class TargetSkillProgressView(LoginRequiredMixin, TenantFilterMixin, ListView):
|
|
"""
|
|
Target skill progress view across all patients.
|
|
|
|
Features:
|
|
- Track specific target skills
|
|
- 0-10 scoring trends
|
|
- Skill mastery analysis
|
|
"""
|
|
model = OTTargetSkill
|
|
template_name = 'ot/target_skill_progress.html'
|
|
context_object_name = 'target_skills'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
"""Get target skills with filters."""
|
|
queryset = OTTargetSkill.objects.filter(
|
|
session__tenant=self.request.user.tenant
|
|
).select_related('session', 'session__patient')
|
|
|
|
# Apply filters
|
|
skill_name = self.request.GET.get('skill_name')
|
|
if skill_name:
|
|
queryset = queryset.filter(skill_name__icontains=skill_name)
|
|
|
|
patient_id = self.request.GET.get('patient')
|
|
if patient_id:
|
|
queryset = queryset.filter(session__patient_id=patient_id)
|
|
|
|
return queryset.order_by('-session__session_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add statistics."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
queryset = self.get_queryset()
|
|
|
|
# Add statistics
|
|
context['stats'] = {
|
|
'total_skills': queryset.count(),
|
|
'avg_score': queryset.aggregate(Avg('score'))['score__avg'],
|
|
'mastered_skills': queryset.filter(score__gte=8).count(),
|
|
}
|
|
|
|
return context
|
|
|
|
|
|
class SkillAssessmentView(LoginRequiredMixin, TenantFilterMixin, ListView):
|
|
"""
|
|
Skill assessment view with enhanced analytics.
|
|
|
|
Features:
|
|
- Detailed skill tracking across patients
|
|
- Achievement level categorization
|
|
- Score percentage visualization
|
|
- Search and filter capabilities
|
|
"""
|
|
model = OTTargetSkill
|
|
template_name = 'ot/skill_assessment.html'
|
|
context_object_name = 'target_skills'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
"""Get target skills with filters and annotations."""
|
|
queryset = OTTargetSkill.objects.filter(
|
|
session__tenant=self.request.user.tenant
|
|
).select_related('session', 'session__patient', 'session__provider')
|
|
|
|
# Apply search filters
|
|
skill_name = self.request.GET.get('skill_name', '').strip()
|
|
if skill_name:
|
|
queryset = queryset.filter(skill_name__icontains=skill_name)
|
|
|
|
patient_search = self.request.GET.get('patient', '').strip()
|
|
if patient_search:
|
|
queryset = queryset.filter(
|
|
Q(session__patient__first_name_en__icontains=patient_search) |
|
|
Q(session__patient__last_name_en__icontains=patient_search) |
|
|
Q(session__patient__mrn__icontains=patient_search)
|
|
)
|
|
|
|
return queryset.order_by('-session__session_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add statistics."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
queryset = self.get_queryset()
|
|
|
|
# Calculate statistics
|
|
total_skills = queryset.count()
|
|
avg_score = queryset.aggregate(Avg('score'))['score__avg'] or 0
|
|
mastered_skills = queryset.filter(score__gte=8).count()
|
|
|
|
context['stats'] = {
|
|
'total_skills': total_skills,
|
|
'avg_score': avg_score,
|
|
'mastered_skills': mastered_skills,
|
|
}
|
|
|
|
# Note: score_percentage and achievement_level are already properties on the model
|
|
# They will be automatically available in the template
|
|
|
|
return context
|
|
|
|
|
|
# ============================================================================
|
|
# PDF Generation Views
|
|
# ============================================================================
|
|
|
|
from core.pdf_service import BasePDFGenerator
|
|
from django.shortcuts import redirect
|
|
|
|
|
|
class OTConsultPDFGenerator(BasePDFGenerator):
|
|
"""PDF generator for OT Consultation (OT-F-1)."""
|
|
|
|
def get_document_title(self):
|
|
"""Return document title in English and Arabic."""
|
|
consult = self.document
|
|
return (
|
|
f"OT Consultation (OT-F-1) - {consult.patient.mrn}",
|
|
"استشارة العلاج الوظيفي"
|
|
)
|
|
|
|
def get_pdf_filename(self):
|
|
"""Return PDF filename."""
|
|
consult = self.document
|
|
date_str = consult.consultation_date.strftime('%Y%m%d')
|
|
return f"ot_consultation_{consult.patient.mrn}_{date_str}.pdf"
|
|
|
|
def get_document_sections(self):
|
|
"""Return document sections to render."""
|
|
consult = self.document
|
|
patient = consult.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", ""),
|
|
]
|
|
})
|
|
|
|
# Consultation Details Section
|
|
sections.append({
|
|
'heading_en': 'Consultation Details',
|
|
'heading_ar': 'تفاصيل الاستشارة',
|
|
'type': 'table',
|
|
'content': [
|
|
('Date', 'التاريخ', consult.consultation_date.strftime('%Y-%m-%d'), ""),
|
|
('Provider', 'مقدم الخدمة', consult.provider.get_full_name() if consult.provider else 'N/A', ""),
|
|
('Recommendation', 'التوصية', consult.get_recommendation_display() if consult.recommendation else 'N/A', ""),
|
|
('Signed By', 'موقع من قبل', consult.signed_by.get_full_name() if consult.signed_by else 'Not signed', ""),
|
|
]
|
|
})
|
|
|
|
# Reasons for Referral
|
|
if consult.reasons:
|
|
sections.append({
|
|
'heading_en': 'Reasons for Referral',
|
|
'heading_ar': 'أسباب الإحالة',
|
|
'type': 'text',
|
|
'content': [consult.reasons]
|
|
})
|
|
|
|
# Top Difficulty Areas
|
|
if consult.top_difficulty_areas:
|
|
sections.append({
|
|
'heading_en': 'Top Difficulty Areas',
|
|
'heading_ar': 'أهم مجالات الصعوبة',
|
|
'type': 'text',
|
|
'content': [consult.top_difficulty_areas]
|
|
})
|
|
|
|
# Developmental Motor Milestones
|
|
if consult.developmental_motor_milestones:
|
|
sections.append({
|
|
'heading_en': 'Developmental Motor Milestones',
|
|
'heading_ar': 'المعالم الحركية التنموية',
|
|
'type': 'text',
|
|
'content': [consult.developmental_motor_milestones]
|
|
})
|
|
|
|
# Self-Help Skills
|
|
if consult.self_help_skills:
|
|
sections.append({
|
|
'heading_en': 'Self-Help Skills',
|
|
'heading_ar': 'مهارات المساعدة الذاتية',
|
|
'type': 'text',
|
|
'content': [consult.self_help_skills]
|
|
})
|
|
|
|
# Feeding Participation
|
|
if consult.feeding_participation:
|
|
sections.append({
|
|
'heading_en': 'Feeding Participation',
|
|
'heading_ar': 'المشاركة في التغذية',
|
|
'type': 'text',
|
|
'content': [consult.feeding_participation]
|
|
})
|
|
|
|
# Behavior Descriptors
|
|
if consult.infant_behavior_descriptors or consult.current_behavior_descriptors:
|
|
behavior_content = []
|
|
if consult.infant_behavior_descriptors:
|
|
behavior_content.append(f"<b>Infant Behavior:</b> {consult.infant_behavior_descriptors}")
|
|
if consult.current_behavior_descriptors:
|
|
behavior_content.append(f"<b>Current Behavior:</b> {consult.current_behavior_descriptors}")
|
|
|
|
sections.append({
|
|
'heading_en': 'Behavior Descriptors',
|
|
'heading_ar': 'وصف السلوك',
|
|
'type': 'text',
|
|
'content': behavior_content
|
|
})
|
|
|
|
# Recommendation Notes
|
|
if consult.recommendation_notes:
|
|
sections.append({
|
|
'heading_en': 'Recommendation Notes',
|
|
'heading_ar': 'ملاحظات التوصية',
|
|
'type': 'text',
|
|
'content': [consult.recommendation_notes]
|
|
})
|
|
|
|
return sections
|
|
|
|
|
|
class OTConsultPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Generate PDF for OT consultation."""
|
|
|
|
def get(self, request, pk):
|
|
"""Generate and return PDF."""
|
|
consult = get_object_or_404(
|
|
OTConsult.objects.select_related(
|
|
'patient', 'provider', 'tenant', 'signed_by'
|
|
),
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
pdf_generator = OTConsultPDFGenerator(consult, request)
|
|
view_mode = request.GET.get('view', 'download')
|
|
return pdf_generator.generate_pdf(view_mode=view_mode)
|
|
|
|
|
|
class OTConsultEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Email OT consultation PDF to patient."""
|
|
|
|
def post(self, request, pk):
|
|
"""Send PDF via email."""
|
|
consult = get_object_or_404(
|
|
OTConsult.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('ot:consult_detail', pk=pk)
|
|
|
|
pdf_generator = OTConsultPDFGenerator(consult, request)
|
|
|
|
subject = f"OT Consultation - {consult.patient.mrn}"
|
|
body = f"""
|
|
Dear {consult.patient.first_name_en} {consult.patient.last_name_en},
|
|
|
|
Please find attached your Occupational Therapy consultation details.
|
|
|
|
Consultation Date: {consult.consultation_date.strftime('%Y-%m-%d')}
|
|
Provider: {consult.provider.get_full_name() if consult.provider else 'N/A'}
|
|
|
|
Best regards,
|
|
{consult.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('ot:consult_detail', pk=pk)
|
|
|
|
|
|
class OTSessionPDFGenerator(BasePDFGenerator):
|
|
"""PDF generator for OT Session (OT-F-3)."""
|
|
|
|
def get_document_title(self):
|
|
"""Return document title in English and Arabic."""
|
|
session = self.document
|
|
return (
|
|
f"OT Session (OT-F-3) - {session.patient.mrn}",
|
|
"جلسة العلاج الوظيفي"
|
|
)
|
|
|
|
def get_pdf_filename(self):
|
|
"""Return PDF filename."""
|
|
session = self.document
|
|
date_str = session.session_date.strftime('%Y%m%d')
|
|
return f"ot_session_{session.patient.mrn}_{date_str}.pdf"
|
|
|
|
def get_document_sections(self):
|
|
"""Return document sections to render."""
|
|
session = self.document
|
|
patient = session.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, ""),
|
|
('Age', 'العمر', f"{patient.age} years", ""),
|
|
]
|
|
})
|
|
|
|
# Session Details Section
|
|
sections.append({
|
|
'heading_en': 'Session Details',
|
|
'heading_ar': 'تفاصيل الجلسة',
|
|
'type': 'table',
|
|
'content': [
|
|
('Date', 'التاريخ', session.session_date.strftime('%Y-%m-%d'), ""),
|
|
('Provider', 'مقدم الخدمة', session.provider.get_full_name() if session.provider else 'N/A', ""),
|
|
('Session Type', 'نوع الجلسة', session.get_session_type_display(), ""),
|
|
('Cooperative Level', 'مستوى التعاون', f"{session.cooperative_level}/4 - {session.cooperative_level_display}" if session.cooperative_level else 'N/A', ""),
|
|
('Distraction Tolerance', 'تحمل التشتت', f"{session.distraction_tolerance}/4 - {session.distraction_tolerance_display}" if session.distraction_tolerance else 'N/A', ""),
|
|
('Signed By', 'موقع من قبل', session.signed_by.get_full_name() if session.signed_by else 'Not signed', ""),
|
|
]
|
|
})
|
|
|
|
# Activities Checklist
|
|
if session.activities_checklist:
|
|
sections.append({
|
|
'heading_en': 'Activities Worked On',
|
|
'heading_ar': 'الأنشطة التي تم العمل عليها',
|
|
'type': 'text',
|
|
'content': [session.activities_checklist]
|
|
})
|
|
|
|
# Target Skills
|
|
target_skills = session.target_skills.all()
|
|
if target_skills:
|
|
skill_content = []
|
|
for skill in target_skills:
|
|
skill_text = f"• <b>{skill.skill_name}</b> - Score: {skill.score}/10 ({skill.achievement_level})"
|
|
if skill.notes:
|
|
skill_text += f"<br/> Notes: {skill.notes}"
|
|
skill_content.append(skill_text)
|
|
|
|
sections.append({
|
|
'heading_en': 'Target Skills Progress',
|
|
'heading_ar': 'تقدم المهارات المستهدفة',
|
|
'type': 'text',
|
|
'content': skill_content
|
|
})
|
|
|
|
# Observations
|
|
if session.observations:
|
|
sections.append({
|
|
'heading_en': 'Observations',
|
|
'heading_ar': 'الملاحظات',
|
|
'type': 'text',
|
|
'content': [session.observations]
|
|
})
|
|
|
|
# Activities Performed
|
|
if session.activities_performed:
|
|
sections.append({
|
|
'heading_en': 'Activities Performed',
|
|
'heading_ar': 'الأنشطة المنفذة',
|
|
'type': 'text',
|
|
'content': [session.activities_performed]
|
|
})
|
|
|
|
# Recommendations
|
|
if session.recommendations:
|
|
sections.append({
|
|
'heading_en': 'Recommendations',
|
|
'heading_ar': 'التوصيات',
|
|
'type': 'text',
|
|
'content': [session.recommendations]
|
|
})
|
|
|
|
return sections
|
|
|
|
|
|
class OTSessionPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Generate PDF for OT session."""
|
|
|
|
def get(self, request, pk):
|
|
"""Generate and return PDF."""
|
|
session = get_object_or_404(
|
|
OTSession.objects.select_related(
|
|
'patient', 'provider', 'tenant', 'signed_by'
|
|
).prefetch_related('target_skills'),
|
|
pk=pk,
|
|
tenant=request.user.tenant
|
|
)
|
|
|
|
pdf_generator = OTSessionPDFGenerator(session, request)
|
|
view_mode = request.GET.get('view', 'download')
|
|
return pdf_generator.generate_pdf(view_mode=view_mode)
|
|
|
|
|
|
class OTSessionEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View):
|
|
"""Email OT session PDF to patient."""
|
|
|
|
def post(self, request, pk):
|
|
"""Send PDF via email."""
|
|
session = get_object_or_404(
|
|
OTSession.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('ot:session_detail', pk=pk)
|
|
|
|
pdf_generator = OTSessionPDFGenerator(session, request)
|
|
|
|
subject = f"OT Session - {session.patient.mrn}"
|
|
body = f"""
|
|
Dear {session.patient.first_name_en} {session.patient.last_name_en},
|
|
|
|
Please find attached your Occupational Therapy session details.
|
|
|
|
Session Date: {session.session_date.strftime('%Y-%m-%d')}
|
|
Provider: {session.provider.get_full_name() if session.provider else 'N/A'}
|
|
|
|
Best regards,
|
|
{session.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('ot:session_detail', pk=pk)
|