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

389 lines
11 KiB
Python

"""
MDT (Multi-Disciplinary Team) models.
This module handles multidisciplinary collaboration, team notes,
and cross-department patient care coordination.
"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
from core.models import (
UUIDPrimaryKeyMixin,
TimeStampedMixin,
TenantOwnedMixin,
)
class MDTNote(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
"""
Multi-Disciplinary Team notes for collaborative patient care.
Allows multiple professionals to contribute to a shared clinical note.
"""
class Status(models.TextChoices):
DRAFT = 'DRAFT', _('Draft')
PENDING_APPROVAL = 'PENDING_APPROVAL', _('Pending Approval')
FINALIZED = 'FINALIZED', _('Finalized')
ARCHIVED = 'ARCHIVED', _('Archived')
patient = models.ForeignKey(
'core.Patient',
on_delete=models.CASCADE,
related_name='mdt_notes',
verbose_name=_("Patient")
)
title = models.CharField(
max_length=200,
verbose_name=_("Title"),
help_text=_("Brief title for this MDT note")
)
purpose = models.TextField(
verbose_name=_("Purpose"),
help_text=_("Purpose of this MDT discussion")
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT,
verbose_name=_("Status")
)
# Contributors
initiated_by = models.ForeignKey(
'core.User',
on_delete=models.SET_NULL,
null=True,
related_name='initiated_mdt_notes',
verbose_name=_("Initiated By")
)
contributors = models.ManyToManyField(
'core.User',
through='MDTContribution',
related_name='contributed_mdt_notes',
verbose_name=_("Contributors")
)
# Finalization (requires 2 seniors from different departments)
finalized_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Finalized At")
)
finalized_by = models.ManyToManyField(
'core.User',
through='MDTApproval',
related_name='finalized_mdt_notes',
verbose_name=_("Finalized By"),
help_text=_("Requires at least 2 seniors from different departments")
)
# Version Control
version = models.PositiveIntegerField(
default=1,
verbose_name=_("Version")
)
# Summary
summary = models.TextField(
blank=True,
verbose_name=_("Summary"),
help_text=_("Final summary of MDT discussion and recommendations")
)
recommendations = models.TextField(
blank=True,
verbose_name=_("Recommendations"),
help_text=_("Action items and recommendations from MDT")
)
history = HistoricalRecords()
class Meta:
verbose_name = _("MDT Note")
verbose_name_plural = _("MDT Notes")
ordering = ['-created_at']
indexes = [
models.Index(fields=['patient', 'status']),
models.Index(fields=['status', 'created_at']),
models.Index(fields=['tenant', 'status']),
]
def __str__(self):
return f"MDT Note: {self.title} - {self.patient}"
@property
def is_editable(self):
"""Check if note can still be edited."""
return self.status in [self.Status.DRAFT, self.Status.PENDING_APPROVAL]
@property
def can_finalize(self):
"""Check if note can be finalized."""
# Requires at least 2 approvals from different departments
approvals = self.approvals.filter(approved=True)
if approvals.count() < 2:
return False
# Check if approvals are from different departments
departments = set()
for approval in approvals:
if approval.approver.role:
departments.add(approval.approver.role)
return len(departments) >= 2
def finalize(self):
"""Finalize the MDT note."""
from django.utils import timezone
if not self.can_finalize:
raise ValueError("Cannot finalize: requires 2 approvals from different departments")
self.status = self.Status.FINALIZED
self.finalized_at = timezone.now()
self.save()
@classmethod
def get_pending_for_user(cls, user):
"""Get MDT notes pending contribution from a specific user."""
return cls.objects.filter(
contributors=user,
status__in=[cls.Status.DRAFT, cls.Status.PENDING_APPROVAL]
).distinct()
class MDTContribution(UUIDPrimaryKeyMixin, TimeStampedMixin):
"""
Individual contributions to an MDT note.
Tracks who contributed what and when.
"""
mdt_note = models.ForeignKey(
MDTNote,
on_delete=models.CASCADE,
related_name='contributions',
verbose_name=_("MDT Note")
)
contributor = models.ForeignKey(
'core.User',
on_delete=models.CASCADE,
related_name='mdt_contributions',
verbose_name=_("Contributor")
)
clinic = models.ForeignKey(
'core.Clinic',
on_delete=models.CASCADE,
related_name='mdt_contributions',
verbose_name=_("Clinic"),
help_text=_("Department/clinic the contributor represents")
)
content = models.TextField(
verbose_name=_("Content"),
help_text=_("Contribution from this professional")
)
is_final = models.BooleanField(
default=False,
verbose_name=_("Is Final"),
help_text=_("Whether this contribution is finalized")
)
edited_at = models.DateTimeField(
auto_now=True,
verbose_name=_("Last Edited At")
)
# Tagging/Mentions
mentioned_users = models.ManyToManyField(
'core.User',
blank=True,
related_name='mdt_mentions',
verbose_name=_("Mentioned Users"),
help_text=_("Users tagged in this contribution")
)
history = HistoricalRecords()
class Meta:
verbose_name = _("MDT Contribution")
verbose_name_plural = _("MDT Contributions")
ordering = ['created_at']
unique_together = [['mdt_note', 'contributor', 'clinic']]
indexes = [
models.Index(fields=['mdt_note', 'created_at']),
models.Index(fields=['contributor', 'created_at']),
]
def __str__(self):
return f"{self.contributor.get_full_name()} - {self.clinic.name_en}"
class MDTApproval(UUIDPrimaryKeyMixin, TimeStampedMixin):
"""
Approval records for MDT notes.
Requires at least 2 senior therapists from different departments.
"""
mdt_note = models.ForeignKey(
MDTNote,
on_delete=models.CASCADE,
related_name='approvals',
verbose_name=_("MDT Note")
)
approver = models.ForeignKey(
'core.User',
on_delete=models.CASCADE,
related_name='mdt_approvals',
verbose_name=_("Approver"),
help_text=_("Must be a Senior Therapist")
)
clinic = models.ForeignKey(
'core.Clinic',
on_delete=models.CASCADE,
related_name='mdt_approvals',
verbose_name=_("Clinic"),
help_text=_("Department the approver represents")
)
approved = models.BooleanField(
default=False,
verbose_name=_("Approved")
)
approved_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Approved At")
)
comments = models.TextField(
blank=True,
verbose_name=_("Comments"),
help_text=_("Optional comments from the approver")
)
class Meta:
verbose_name = _("MDT Approval")
verbose_name_plural = _("MDT Approvals")
ordering = ['approved_at']
unique_together = [['mdt_note', 'approver']]
indexes = [
models.Index(fields=['mdt_note', 'approved']),
models.Index(fields=['approver', 'approved_at']),
]
def __str__(self):
status = "Approved" if self.approved else "Pending"
return f"{self.approver.get_full_name()} - {status}"
def approve(self, comments=""):
"""Approve the MDT note."""
from django.utils import timezone
self.approved = True
self.approved_at = timezone.now()
self.comments = comments
self.save()
# Check if MDT note can be finalized
if self.mdt_note.can_finalize and self.mdt_note.status != MDTNote.Status.FINALIZED:
self.mdt_note.status = MDTNote.Status.PENDING_APPROVAL
self.mdt_note.save()
class MDTMention(UUIDPrimaryKeyMixin, TimeStampedMixin):
"""
Tracks when users are mentioned/tagged in MDT contributions.
Used for notifications and access control.
"""
contribution = models.ForeignKey(
MDTContribution,
on_delete=models.CASCADE,
related_name='mentions',
verbose_name=_("Contribution")
)
mentioned_user = models.ForeignKey(
'core.User',
on_delete=models.CASCADE,
related_name='received_mdt_mentions',
verbose_name=_("Mentioned User")
)
notified_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Notified At")
)
viewed_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Viewed At")
)
class Meta:
verbose_name = _("MDT Mention")
verbose_name_plural = _("MDT Mentions")
ordering = ['-created_at']
unique_together = [['contribution', 'mentioned_user']]
indexes = [
models.Index(fields=['mentioned_user', 'viewed_at']),
]
def __str__(self):
return f"@{self.mentioned_user.username} in {self.contribution.mdt_note.title}"
def mark_as_viewed(self):
"""Mark mention as viewed."""
from django.utils import timezone
if not self.viewed_at:
self.viewed_at = timezone.now()
self.save(update_fields=['viewed_at'])
class MDTAttachment(UUIDPrimaryKeyMixin, TimeStampedMixin):
"""
Attachments for MDT notes (reports, images, documents).
"""
class FileType(models.TextChoices):
REPORT = 'REPORT', _('Report')
IMAGE = 'IMAGE', _('Image')
DOCUMENT = 'DOCUMENT', _('Document')
LAB_RESULT = 'LAB_RESULT', _('Lab Result')
ASSESSMENT = 'ASSESSMENT', _('Assessment')
OTHER = 'OTHER', _('Other')
mdt_note = models.ForeignKey(
MDTNote,
on_delete=models.CASCADE,
related_name='attachments',
verbose_name=_("MDT Note")
)
file = models.FileField(
upload_to='mdt/attachments/%Y/%m/%d/',
verbose_name=_("File")
)
file_type = models.CharField(
max_length=20,
choices=FileType.choices,
verbose_name=_("File Type")
)
description = models.TextField(
blank=True,
verbose_name=_("Description")
)
uploaded_by = models.ForeignKey(
'core.User',
on_delete=models.SET_NULL,
null=True,
related_name='uploaded_mdt_attachments',
verbose_name=_("Uploaded By")
)
class Meta:
verbose_name = _("MDT Attachment")
verbose_name_plural = _("MDT Attachments")
ordering = ['-created_at']
indexes = [
models.Index(fields=['mdt_note', 'created_at']),
]
def __str__(self):
return f"{self.file.name} - {self.mdt_note.title}"