""" Business logic services for MDT app. """ from django.db.models import Q, Count, Prefetch from django.utils import timezone from datetime import timedelta from typing import List, Dict, Optional from .models import ( MDTNote, MDTContribution, MDTApproval, MDTMention, MDTAttachment, ) from core.models import Patient, User, Clinic from notifications.models import Notification class MDTNoteManagementService: """ Service for managing MDT notes lifecycle and workflows. """ @staticmethod def create_mdt_note( patient: Patient, title: str, purpose: str, initiated_by: User, tenant ) -> MDTNote: """ Create a new MDT note. Args: patient: Patient object title: Note title purpose: Purpose of MDT discussion initiated_by: User who initiated the note tenant: Tenant object Returns: MDTNote object """ mdt_note = MDTNote.objects.create( patient=patient, tenant=tenant, title=title, purpose=purpose, initiated_by=initiated_by, status=MDTNote.Status.DRAFT, ) return mdt_note @staticmethod def add_contribution( mdt_note: MDTNote, contributor: User, clinic: Clinic, content: str, mentioned_users: Optional[List[User]] = None ) -> MDTContribution: """ Add a contribution to an MDT note. Args: mdt_note: MDTNote object contributor: User making the contribution clinic: Clinic the contributor represents content: Contribution content mentioned_users: Optional list of users to mention Returns: MDTContribution object """ if not mdt_note.is_editable: raise ValueError("Cannot add contributions to finalized or archived notes") contribution, created = MDTContribution.objects.get_or_create( mdt_note=mdt_note, contributor=contributor, clinic=clinic, defaults={'content': content} ) if not created: # Update existing contribution contribution.content = content contribution.save() # Add mentions if mentioned_users: contribution.mentioned_users.set(mentioned_users) # Create mention records and notifications for user in mentioned_users: mention, _ = MDTMention.objects.get_or_create( contribution=contribution, mentioned_user=user ) # Send notification Notification.objects.create( user=user, notification_type='MDT_MENTION', title=f"Mentioned in MDT Note: {mdt_note.title}", message=f"{contributor.get_full_name()} mentioned you in an MDT note for {mdt_note.patient.get_full_name()}", ) mention.notified_at = timezone.now() mention.save() return contribution @staticmethod def request_approval(mdt_note: MDTNote, approvers: List[tuple]) -> List[MDTApproval]: """ Request approval from senior therapists. Args: mdt_note: MDTNote object approvers: List of (user, clinic) tuples Returns: List of MDTApproval objects """ if mdt_note.status == MDTNote.Status.FINALIZED: raise ValueError("Cannot request approval for finalized notes") approvals = [] for approver, clinic in approvers: approval, created = MDTApproval.objects.get_or_create( mdt_note=mdt_note, approver=approver, defaults={'clinic': clinic} ) approvals.append(approval) if created: # Send notification Notification.objects.create( user=approver, notification_type='MDT_APPROVAL_REQUEST', title=f"MDT Approval Requested: {mdt_note.title}", message=f"Your approval is requested for MDT note regarding {mdt_note.patient.get_full_name()}", ) # Update status to pending approval if mdt_note.status == MDTNote.Status.DRAFT: mdt_note.status = MDTNote.Status.PENDING_APPROVAL mdt_note.save() return approvals @staticmethod def get_pending_notes_for_user(user: User) -> List[MDTNote]: """ Get MDT notes pending contribution from user. Args: user: User object Returns: List of MDTNote objects """ return list(MDTNote.get_pending_for_user(user)) @staticmethod def get_notes_requiring_approval(user: User) -> List[MDTNote]: """ Get MDT notes requiring approval from user. Args: user: User object Returns: List of MDTNote objects """ pending_approvals = MDTApproval.objects.filter( approver=user, approved=False ).values_list('mdt_note_id', flat=True) return list(MDTNote.objects.filter(id__in=pending_approvals)) class MDTCollaborationService: """ Service for managing MDT collaboration workflows. """ @staticmethod def get_collaboration_summary(mdt_note: MDTNote) -> Dict[str, any]: """ Get summary of collaboration on an MDT note. Returns: dict: Collaboration summary """ contributions = mdt_note.contributions.all() approvals = mdt_note.approvals.all() return { 'total_contributors': contributions.count(), 'departments_involved': contributions.values('clinic').distinct().count(), 'final_contributions': contributions.filter(is_final=True).count(), 'pending_contributions': contributions.filter(is_final=False).count(), 'total_approvals_requested': approvals.count(), 'approvals_received': approvals.filter(approved=True).count(), 'approvals_pending': approvals.filter(approved=False).count(), 'can_finalize': mdt_note.can_finalize, 'is_editable': mdt_note.is_editable, } @staticmethod def get_department_participation(tenant_id: str) -> Dict[str, int]: """ Get MDT participation by department. Args: tenant_id: Tenant ID Returns: dict: Department participation counts """ contributions = MDTContribution.objects.filter( mdt_note__tenant_id=tenant_id ) return dict( contributions.values('clinic__name_en').annotate( count=Count('id') ).values_list('clinic__name_en', 'count') ) @staticmethod def check_approval_requirements(mdt_note: MDTNote) -> Dict[str, any]: """ Check if MDT note meets approval requirements. Returns: dict: Approval status """ approvals = mdt_note.approvals.filter(approved=True) # Get unique departments departments = set() for approval in approvals: if approval.clinic: departments.add(approval.clinic.id) return { 'total_approvals': approvals.count(), 'unique_departments': len(departments), 'meets_requirements': len(departments) >= 2 and approvals.count() >= 2, 'can_finalize': mdt_note.can_finalize, 'missing_approvals': max(0, 2 - approvals.count()), 'missing_departments': max(0, 2 - len(departments)), } class MDTNotificationService: """ Service for managing MDT-specific notifications. """ @staticmethod def notify_contributors(mdt_note: MDTNote, message: str) -> int: """ Notify all contributors of an MDT note. Returns: int: Number of notifications sent """ contributors = mdt_note.contributors.all().distinct() notifications_sent = 0 for contributor in contributors: Notification.objects.create( user=contributor, notification_type='MDT_UPDATE', title=f"MDT Note Update: {mdt_note.title}", message=message, ) notifications_sent += 1 return notifications_sent @staticmethod def notify_finalization(mdt_note: MDTNote) -> int: """ Notify all contributors when note is finalized. Returns: int: Number of notifications sent """ message = f"MDT note '{mdt_note.title}' for {mdt_note.patient.get_full_name()} has been finalized." return MDTNotificationService.notify_contributors(mdt_note, message) @staticmethod def notify_mention(mention: MDTMention) -> None: """ Send notification for a mention. """ Notification.objects.create( user=mention.mentioned_user, notification_type='MDT_MENTION', title=f"Mentioned in MDT Note", message=f"You were mentioned in MDT note: {mention.contribution.mdt_note.title}", ) mention.notified_at = timezone.now() mention.save() class MDTStatisticsService: """ Service for generating MDT statistics and analytics. """ @staticmethod def get_tenant_statistics(tenant_id: str, start_date: Optional[timezone.datetime.date] = None) -> Dict[str, any]: """ Get MDT statistics for a tenant. Args: tenant_id: Tenant ID start_date: Optional start date for filtering Returns: dict: Tenant-wide MDT statistics """ if start_date is None: start_date = timezone.now().date() - timedelta(days=90) notes = MDTNote.objects.filter( tenant_id=tenant_id, created_at__gte=start_date ) contributions = MDTContribution.objects.filter( mdt_note__tenant_id=tenant_id, created_at__gte=start_date ) return { 'period_start': start_date, 'period_end': timezone.now().date(), 'total_notes': notes.count(), 'draft_notes': notes.filter(status=MDTNote.Status.DRAFT).count(), 'pending_approval': notes.filter(status=MDTNote.Status.PENDING_APPROVAL).count(), 'finalized_notes': notes.filter(status=MDTNote.Status.FINALIZED).count(), 'archived_notes': notes.filter(status=MDTNote.Status.ARCHIVED).count(), 'total_contributions': contributions.count(), 'unique_contributors': contributions.values('contributor').distinct().count(), 'departments_involved': contributions.values('clinic').distinct().count(), 'average_contributions_per_note': contributions.count() / notes.count() if notes.count() > 0 else 0, 'notes_by_status': dict( notes.values('status').annotate(count=Count('id')).values_list('status', 'count') ), } @staticmethod def get_user_statistics(user: User, start_date: Optional[timezone.datetime.date] = None) -> Dict[str, any]: """ Get MDT statistics for a specific user. Args: user: User object start_date: Optional start date for filtering Returns: dict: User MDT statistics """ if start_date is None: start_date = timezone.now().date() - timedelta(days=30) # Notes initiated by user initiated_notes = MDTNote.objects.filter( initiated_by=user, created_at__gte=start_date ) # Contributions by user contributions = MDTContribution.objects.filter( contributor=user, created_at__gte=start_date ) # Approvals by user approvals = MDTApproval.objects.filter( approver=user, created_at__gte=start_date ) # Mentions of user mentions = MDTMention.objects.filter( mentioned_user=user, created_at__gte=start_date ) return { 'period_start': start_date, 'period_end': timezone.now().date(), 'notes_initiated': initiated_notes.count(), 'contributions_made': contributions.count(), 'approvals_given': approvals.filter(approved=True).count(), 'approvals_pending': approvals.filter(approved=False).count(), 'times_mentioned': mentions.count(), 'unread_mentions': mentions.filter(viewed_at__isnull=True).count(), 'unique_patients': contributions.values('mdt_note__patient').distinct().count(), } class MDTWorkflowService: """ Service for managing MDT workflow automation. """ @staticmethod def check_and_auto_finalize(mdt_note: MDTNote) -> bool: """ Check if note can be auto-finalized and finalize if ready. Returns: bool: True if finalized """ if mdt_note.can_finalize and mdt_note.status == MDTNote.Status.PENDING_APPROVAL: mdt_note.finalize() # Notify all contributors MDTNotificationService.notify_finalization(mdt_note) return True return False @staticmethod def get_stale_notes(days: int = 30) -> List[MDTNote]: """ Get MDT notes that have been in draft for too long. Args: days: Number of days to consider stale Returns: List of stale MDTNote objects """ cutoff_date = timezone.now() - timedelta(days=days) return list( MDTNote.objects.filter( status=MDTNote.Status.DRAFT, created_at__lt=cutoff_date ).select_related('patient', 'initiated_by') ) @staticmethod def remind_pending_contributors(mdt_note: MDTNote) -> int: """ Send reminders to contributors who haven't finalized their contributions. Returns: int: Number of reminders sent """ pending_contributions = mdt_note.contributions.filter(is_final=False) reminders_sent = 0 for contribution in pending_contributions: Notification.objects.create( user=contribution.contributor, notification_type='MDT_REMINDER', title=f"MDT Contribution Pending: {mdt_note.title}", message=f"Please finalize your contribution to the MDT note for {mdt_note.patient.get_full_name()}", ) reminders_sent += 1 return reminders_sent @staticmethod def remind_pending_approvers(mdt_note: MDTNote) -> int: """ Send reminders to approvers who haven't approved yet. Returns: int: Number of reminders sent """ pending_approvals = mdt_note.approvals.filter(approved=False) reminders_sent = 0 for approval in pending_approvals: Notification.objects.create( user=approval.approver, notification_type='MDT_APPROVAL_REMINDER', title=f"MDT Approval Pending: {mdt_note.title}", message=f"Please review and approve the MDT note for {mdt_note.patient.get_full_name()}", ) reminders_sent += 1 return reminders_sent class MDTReportService: """ Service for generating MDT reports and exports. """ @staticmethod def generate_mdt_summary(mdt_note: MDTNote) -> Dict[str, any]: """ Generate a comprehensive summary of an MDT note. Returns: dict: MDT note summary """ contributions = mdt_note.contributions.all().select_related('contributor', 'clinic') approvals = mdt_note.approvals.filter(approved=True).select_related('approver', 'clinic') # Build contribution summary contribution_summary = [] for contrib in contributions: contribution_summary.append({ 'department': contrib.clinic.name_en, 'contributor': contrib.contributor.get_full_name(), 'content': contrib.content, 'is_final': contrib.is_final, 'date': contrib.created_at, }) # Build approval summary approval_summary = [] for approval in approvals: approval_summary.append({ 'department': approval.clinic.name_en, 'approver': approval.approver.get_full_name(), 'approved_at': approval.approved_at, 'comments': approval.comments, }) return { 'note_id': str(mdt_note.id), 'patient': { 'name': mdt_note.patient.get_full_name(), 'mrn': mdt_note.patient.mrn, }, 'title': mdt_note.title, 'purpose': mdt_note.purpose, 'status': mdt_note.get_status_display(), 'initiated_by': mdt_note.initiated_by.get_full_name() if mdt_note.initiated_by else None, 'initiated_date': mdt_note.created_at, 'finalized_date': mdt_note.finalized_at, 'contributions': contribution_summary, 'approvals': approval_summary, 'summary': mdt_note.summary, 'recommendations': mdt_note.recommendations, 'version': mdt_note.version, } @staticmethod def export_to_pdf(mdt_note: MDTNote) -> bytes: """ Export MDT note to PDF. Returns: bytes: PDF content """ from core.pdf_service import PDFService from io import BytesIO # Generate summary summary = MDTReportService.generate_mdt_summary(mdt_note) # Create HTML content html_content = f"""

Multidisciplinary Team Note

Patient: {summary['patient']['name']} (MRN: {summary['patient']['mrn']})

Title: {summary['title']}

Status: {summary['status']}

Initiated By: {summary['initiated_by'] or 'N/A'}

Initiated Date: {summary['initiated_date'].strftime('%Y-%m-%d %H:%M')}

{f"

Finalized Date: {summary['finalized_date'].strftime('%Y-%m-%d %H:%M')}

" if summary['finalized_date'] else ''}

Purpose

{summary['purpose']}

Contributions

{''.join([f'''

{contrib['department']} - {contrib['contributor']}

{contrib['date'].strftime('%Y-%m-%d %H:%M')}

{contrib['content']}

Status: {'Final' if contrib['is_final'] else 'Draft'}

''' for contrib in summary['contributions']])}

Approvals

{''.join([f'''

{approval['department']} - {approval['approver']}

Approved: {approval['approved_at'].strftime('%Y-%m-%d %H:%M')}

{f"

Comments: {approval['comments']}

" if approval['comments'] else ''}
''' for approval in summary['approvals']])}
{f'''

Summary

{summary['summary']}

''' if summary['summary'] else ''} {f'''

Recommendations

{summary['recommendations']}

''' if summary['recommendations'] else ''}

Version: {summary['version']}

Generated: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}

""" # Generate PDF pdf_content = PDFService.generate_pdf_from_html(html_content) return pdf_content