586 lines
19 KiB
Python
586 lines
19 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.db.models import Q, Avg
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils import timezone
|
|
from django.views.generic import ListView, DetailView, CreateView, UpdateView
|
|
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 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
|
|
"""
|
|
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=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').order_by('-consultation_date')
|
|
|
|
|
|
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
|
|
"""
|
|
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=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').order_by('-session_date')
|
|
|
|
|
|
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
|