agdar/mdt/services.py
Marwan Alwali 2f1681b18c update
2025-11-11 13:44:48 +03:00

661 lines
22 KiB
Python

"""
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"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
h1 {{ color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }}
h2 {{ color: #34495e; margin-top: 30px; }}
.section {{ margin: 20px 0; }}
.label {{ font-weight: bold; color: #7f8c8d; }}
.contribution {{ background: #ecf0f1; padding: 15px; margin: 10px 0; border-left: 4px solid #3498db; }}
.approval {{ background: #d5f4e6; padding: 15px; margin: 10px 0; border-left: 4px solid #27ae60; }}
table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
th, td {{ padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }}
th {{ background-color: #3498db; color: white; }}
</style>
</head>
<body>
<h1>Multidisciplinary Team Note</h1>
<div class="section">
<p><span class="label">Patient:</span> {summary['patient']['name']} (MRN: {summary['patient']['mrn']})</p>
<p><span class="label">Title:</span> {summary['title']}</p>
<p><span class="label">Status:</span> {summary['status']}</p>
<p><span class="label">Initiated By:</span> {summary['initiated_by'] or 'N/A'}</p>
<p><span class="label">Initiated Date:</span> {summary['initiated_date'].strftime('%Y-%m-%d %H:%M')}</p>
{f"<p><span class='label'>Finalized Date:</span> {summary['finalized_date'].strftime('%Y-%m-%d %H:%M')}</p>" if summary['finalized_date'] else ''}
</div>
<div class="section">
<h2>Purpose</h2>
<p>{summary['purpose']}</p>
</div>
<div class="section">
<h2>Contributions</h2>
{''.join([f'''
<div class="contribution">
<p><strong>{contrib['department']} - {contrib['contributor']}</strong></p>
<p><em>{contrib['date'].strftime('%Y-%m-%d %H:%M')}</em></p>
<p>{contrib['content']}</p>
<p><span class="label">Status:</span> {'Final' if contrib['is_final'] else 'Draft'}</p>
</div>
''' for contrib in summary['contributions']])}
</div>
<div class="section">
<h2>Approvals</h2>
{''.join([f'''
<div class="approval">
<p><strong>{approval['department']} - {approval['approver']}</strong></p>
<p><em>Approved: {approval['approved_at'].strftime('%Y-%m-%d %H:%M')}</em></p>
{f"<p><strong>Comments:</strong> {approval['comments']}</p>" if approval['comments'] else ''}
</div>
''' for approval in summary['approvals']])}
</div>
{f'''
<div class="section">
<h2>Summary</h2>
<p>{summary['summary']}</p>
</div>
''' if summary['summary'] else ''}
{f'''
<div class="section">
<h2>Recommendations</h2>
<p>{summary['recommendations']}</p>
</div>
''' if summary['recommendations'] else ''}
<div class="section">
<p><span class="label">Version:</span> {summary['version']}</p>
<p><span class="label">Generated:</span> {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</body>
</html>
"""
# Generate PDF
pdf_content = PDFService.generate_pdf_from_html(html_content)
return pdf_content