""" 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, SignedDocumentEditPreventionMixin, ) from core.models import User, Patient from appointments.models import Appointment from .models import OTConsult, OTSession, OTTargetSkill from .forms import ( OTConsultForm, OTSessionForm, OTDifficultyAreaFormSet, OTMilestoneFormSet, OTSelfHelpSkillFormSet, OTInfantBehaviorFormSet, OTCurrentBehaviorFormSet ) from .scoring_service import OTScoringService, initialize_consultation_data 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! This document can no longer be edited." ) 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! This document can no longer be edited." ) 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: - Comprehensive form with formsets for all related data - Automatic initialization of milestones, skills, and behaviors - Automatic score calculation on save - 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 edit page to complete sections 4-5.""" messages.info(self.request, "Consultation saved! Please complete sections 4-5 (Developmental History & Self-Help Skills) by clicking Edit.") return reverse_lazy('ot:consult_update', 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_context_data(self, **kwargs): """Add formsets and patient/appointment info.""" context = super().get_context_data(**kwargs) context['form_title'] = 'OT Consultation (OT-F-1)' context['submit_text'] = 'Save & Continue to Sections 4-5' # Add formsets (will be empty for new consultations - that's OK) if self.request.POST: context['difficulty_formset'] = OTDifficultyAreaFormSet(self.request.POST, instance=self.object) context['milestone_formset'] = OTMilestoneFormSet(self.request.POST, instance=self.object) context['selfhelp_formset'] = OTSelfHelpSkillFormSet(self.request.POST, instance=self.object) context['infant_behavior_formset'] = OTInfantBehaviorFormSet(self.request.POST, instance=self.object) context['current_behavior_formset'] = OTCurrentBehaviorFormSet(self.request.POST, instance=self.object) else: context['difficulty_formset'] = OTDifficultyAreaFormSet(instance=self.object) context['milestone_formset'] = OTMilestoneFormSet(instance=self.object) context['selfhelp_formset'] = OTSelfHelpSkillFormSet(instance=self.object) context['infant_behavior_formset'] = OTInfantBehaviorFormSet(instance=self.object) context['current_behavior_formset'] = OTCurrentBehaviorFormSet(instance=self.object) # 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 def form_valid(self, form): """Set tenant, provider, and handle formsets.""" context = self.get_context_data() difficulty_formset = context['difficulty_formset'] milestone_formset = context['milestone_formset'] selfhelp_formset = context['selfhelp_formset'] infant_behavior_formset = context['infant_behavior_formset'] current_behavior_formset = context['current_behavior_formset'] # Validate all formsets if not all([ difficulty_formset.is_valid(), milestone_formset.is_valid(), selfhelp_formset.is_valid(), infant_behavior_formset.is_valid(), current_behavior_formset.is_valid() ]): return self.form_invalid(form) # Set tenant and provider form.instance.tenant = self.request.user.tenant 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 self.object = form.save() # Initialize consultation data if this is a new consultation initialize_consultation_data(self.object) # Save formsets difficulty_formset.instance = self.object difficulty_formset.save() milestone_formset.instance = self.object milestone_formset.save() selfhelp_formset.instance = self.object selfhelp_formset.save() infant_behavior_formset.instance = self.object infant_behavior_formset.save() current_behavior_formset.instance = self.object current_behavior_formset.save() # Calculate and save scores scoring_service = OTScoringService(self.object) scoring_service.save_scores() messages.success(self.request, self.success_message) return HttpResponseRedirect(self.get_success_url()) class OTConsultUpdateView(SignedDocumentEditPreventionMixin, LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """ OT consultation update view (OT-F-1). Features: - Update consultation details with formsets - Automatic score recalculation on save - Version history - Audit trail - Prevents editing of signed documents """ 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 formsets, 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 formsets if self.request.POST: context['difficulty_formset'] = OTDifficultyAreaFormSet(self.request.POST, instance=self.object) context['milestone_formset'] = OTMilestoneFormSet(self.request.POST, instance=self.object) context['selfhelp_formset'] = OTSelfHelpSkillFormSet(self.request.POST, instance=self.object) context['infant_behavior_formset'] = OTInfantBehaviorFormSet(self.request.POST, instance=self.object) context['current_behavior_formset'] = OTCurrentBehaviorFormSet(self.request.POST, instance=self.object) else: context['difficulty_formset'] = OTDifficultyAreaFormSet(instance=self.object) context['milestone_formset'] = OTMilestoneFormSet(instance=self.object) context['selfhelp_formset'] = OTSelfHelpSkillFormSet(instance=self.object) context['infant_behavior_formset'] = OTInfantBehaviorFormSet(instance=self.object) context['current_behavior_formset'] = OTCurrentBehaviorFormSet(instance=self.object) # Add version history if available if hasattr(self.object, 'history'): context['history'] = self.object.history.all()[:10] # Add patient info context['patient'] = self.object.patient return context def form_valid(self, form): """Handle formsets and recalculate scores.""" context = self.get_context_data() difficulty_formset = context['difficulty_formset'] milestone_formset = context['milestone_formset'] selfhelp_formset = context['selfhelp_formset'] infant_behavior_formset = context['infant_behavior_formset'] current_behavior_formset = context['current_behavior_formset'] # Validate all formsets if not all([ difficulty_formset.is_valid(), milestone_formset.is_valid(), selfhelp_formset.is_valid(), infant_behavior_formset.is_valid(), current_behavior_formset.is_valid() ]): return self.form_invalid(form) # Save consultation self.object = form.save() # Save formsets difficulty_formset.instance = self.object difficulty_formset.save() milestone_formset.instance = self.object milestone_formset.save() selfhelp_formset.instance = self.object selfhelp_formset.save() infant_behavior_formset.instance = self.object infant_behavior_formset.save() current_behavior_formset.instance = self.object current_behavior_formset.save() # Recalculate and save scores scoring_service = OTScoringService(self.object) scoring_service.save_scores() messages.success(self.request, self.success_message) return HttpResponseRedirect(self.get_success_url()) 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(SignedDocumentEditPreventionMixin, LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """ OT session update view (OT-F-3). Features: - Update session details - Version history - Audit trail - Prevents editing of signed documents """ 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', ""), ] }) # Referral Reason if consult.referral_reason: sections.append({ 'heading_en': 'Reason for Referral', 'heading_ar': 'سبب الإحالة', 'type': 'text', 'content': [consult.get_referral_reason_display()] }) # Difficulty Areas if consult.difficulty_areas.exists(): difficulty_content = [] for area in consult.difficulty_areas.all(): text = f"• {area.get_area_display()}" if area.details: text += f": {area.details}" difficulty_content.append(text) sections.append({ 'heading_en': 'Areas of Difficulty', 'heading_ar': 'مجالات الصعوبة', 'type': 'text', 'content': difficulty_content }) # Developmental Milestones if consult.milestones.exists(): milestone_content = [] for milestone in consult.milestones.all(): if milestone.age_achieved: required_badge = " (Required)" if milestone.is_required else "" milestone_content.append(f"• {milestone.get_milestone_display()}{required_badge}: {milestone.age_achieved}") if milestone_content: sections.append({ 'heading_en': 'Developmental Motor Milestones', 'heading_ar': 'المعالم الحركية التنموية', 'type': 'text', 'content': milestone_content }) # Motor Learning & Regression if consult.motor_learning_difficulty is not None or consult.motor_skill_regression is not None: motor_content = [] if consult.motor_learning_difficulty is not None: motor_content.append(f"Difficulty learning new motor skills: {'Yes' if consult.motor_learning_difficulty else 'No'}") if consult.motor_learning_details: motor_content.append(f" Details: {consult.motor_learning_details}") if consult.motor_skill_regression is not None: motor_content.append(f"Lost previously gained motor skills: {'Yes' if consult.motor_skill_regression else 'No'}") if consult.regression_details: motor_content.append(f" Details: {consult.regression_details}") sections.append({ 'heading_en': 'Motor Learning & Regression', 'heading_ar': 'التعلم الحركي والتراجع', 'type': 'text', 'content': motor_content }) # Self-Help Skills if consult.self_help_skills.exists(): skill_content = [] for skill in consult.self_help_skills.all(): if skill.response: response_text = 'Yes' if skill.response == 'yes' else 'No' text = f"• {skill.skill_name} ({skill.get_age_range_display()}): {response_text}" if skill.comments: text += f" - {skill.comments}" skill_content.append(text) if skill_content: sections.append({ 'heading_en': 'Self-Help Skills', 'heading_ar': 'مهارات المساعدة الذاتية', 'type': 'text', 'content': skill_content }) # Eating/Feeding eating_content = [] if consult.eats_healthy_variety is not None: eating_content.append(f"Eats healthy variety: {'Yes' if consult.eats_healthy_variety else 'No'}") if consult.eats_variety_textures is not None: eating_content.append(f"Eats variety of textures: {'Yes' if consult.eats_variety_textures else 'No'}") if consult.participates_family_meals is not None: eating_content.append(f"Participates in family meals: {'Yes' if consult.participates_family_meals else 'No'}") if consult.eating_comments: eating_content.append(f"Comments: {consult.eating_comments}") if eating_content: sections.append({ 'heading_en': 'Eating / Feeding', 'heading_ar': 'الأكل / التغذية', 'type': 'text', 'content': eating_content }) # Behavior Comments behavior_content = [] if consult.infant_behavior_comments: behavior_content.append(f"Infant Behavior: {consult.infant_behavior_comments}") if consult.current_behavior_comments: behavior_content.append(f"Current Behavior: {consult.current_behavior_comments}") if behavior_content: sections.append({ 'heading_en': 'Behavior Comments', 'heading_ar': 'تعليقات السلوك', 'type': 'text', 'content': behavior_content }) # Scoring Results if consult.total_score > 0: score_content = [ f"Self-Help Score: {consult.self_help_score}/24", f"Behavior Score: {consult.behavior_score}/48", f"Developmental Score: {consult.developmental_score}/6", f"Eating Score: {consult.eating_score}/6", f"Total Score: {consult.total_score}/84", f"Interpretation: {consult.score_interpretation}" ] sections.append({ 'heading_en': 'Scoring Results', 'heading_ar': 'نتائج التقييم', 'type': 'text', 'content': score_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"• {skill.skill_name} - Score: {skill.score}/10 ({skill.achievement_level})" if skill.notes: skill_text += f"
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)