661 lines
22 KiB
Python
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
|