389 lines
11 KiB
Python
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}"
|