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

283 lines
8.8 KiB
Python

"""
Documentation Delay Tracking Service.
This service monitors clinical documentation completion and alerts
senior therapists when documentation is delayed beyond 5 working days.
"""
from datetime import datetime, timedelta
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin
class DocumentationDelayTracker(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
"""
Tracks documentation delays and sends alerts to senior therapists.
"""
class DocumentType(models.TextChoices):
SESSION_NOTE = 'SESSION_NOTE', _('Session Note')
ASSESSMENT = 'ASSESSMENT', _('Assessment')
PROGRESS_REPORT = 'PROGRESS_REPORT', _('Progress Report')
DISCHARGE_SUMMARY = 'DISCHARGE_SUMMARY', _('Discharge Summary')
MDT_NOTE = 'MDT_NOTE', _('MDT Note')
class Status(models.TextChoices):
PENDING = 'PENDING', _('Pending')
OVERDUE = 'OVERDUE', _('Overdue')
COMPLETED = 'COMPLETED', _('Completed')
ESCALATED = 'ESCALATED', _('Escalated')
# Document Reference (Generic FK)
document_type = models.CharField(
max_length=30,
choices=DocumentType.choices,
verbose_name=_("Document Type")
)
document_id = models.UUIDField(
verbose_name=_("Document ID"),
help_text=_("UUID of the document being tracked")
)
# Responsible Staff
assigned_to = models.ForeignKey(
'core.User',
on_delete=models.CASCADE,
related_name='assigned_documentation',
verbose_name=_("Assigned To"),
help_text=_("Therapist responsible for completing the documentation")
)
senior_therapist = models.ForeignKey(
'core.User',
on_delete=models.CASCADE,
related_name='supervised_documentation',
verbose_name=_("Senior Therapist"),
help_text=_("Senior therapist to be notified of delays")
)
# Timing
due_date = models.DateField(
verbose_name=_("Due Date"),
help_text=_("Date by which documentation should be completed")
)
completed_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Completed At")
)
# Status & Alerts
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.PENDING,
verbose_name=_("Status")
)
days_overdue = models.IntegerField(
default=0,
verbose_name=_("Days Overdue")
)
alert_sent_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Alert Sent At")
)
alert_count = models.PositiveIntegerField(
default=0,
verbose_name=_("Alert Count"),
help_text=_("Number of alerts sent to senior")
)
last_alert_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Last Alert At")
)
# Escalation
escalated_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Escalated At")
)
escalated_to = models.ForeignKey(
'core.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='escalated_documentation',
verbose_name=_("Escalated To"),
help_text=_("Clinical Coordinator or Admin if escalated")
)
# Notes
notes = models.TextField(
blank=True,
verbose_name=_("Notes")
)
class Meta:
verbose_name = _("Documentation Delay Tracker")
verbose_name_plural = _("Documentation Delay Trackers")
ordering = ['-days_overdue', 'due_date']
indexes = [
models.Index(fields=['assigned_to', 'status']),
models.Index(fields=['senior_therapist', 'status']),
models.Index(fields=['status', 'due_date']),
models.Index(fields=['tenant', 'status']),
]
def __str__(self):
return f"{self.get_document_type_display()} - {self.assigned_to} - {self.days_overdue} days overdue"
def calculate_days_overdue(self):
"""Calculate how many working days the documentation is overdue."""
from datetime import date
if self.status == self.Status.COMPLETED:
return 0
today = date.today()
if today <= self.due_date:
return 0
# Calculate working days (excluding weekends)
days = 0
current_date = self.due_date + timedelta(days=1)
while current_date <= today:
# Skip Friday and Saturday (weekend in Saudi Arabia)
if current_date.weekday() not in [4, 5]: # 4=Friday, 5=Saturday
days += 1
current_date += timedelta(days=1)
return days
def update_status(self):
"""Update status based on current state."""
self.days_overdue = self.calculate_days_overdue()
if self.status == self.Status.COMPLETED:
return
if self.days_overdue >= 5:
if self.days_overdue >= 10 and not self.escalated_at:
self.status = self.Status.ESCALATED
else:
self.status = self.Status.OVERDUE
else:
self.status = self.Status.PENDING
self.save(update_fields=['status', 'days_overdue'])
def mark_completed(self):
"""Mark documentation as completed."""
self.status = self.Status.COMPLETED
self.completed_at = timezone.now()
self.days_overdue = 0
self.save()
def send_alert(self):
"""Record that an alert was sent."""
self.alert_count += 1
self.last_alert_at = timezone.now()
if self.alert_count == 1:
self.alert_sent_at = self.last_alert_at
self.save(update_fields=['alert_count', 'last_alert_at', 'alert_sent_at'])
def escalate(self, escalated_to_user):
"""Escalate to Clinical Coordinator or Admin."""
self.status = self.Status.ESCALATED
self.escalated_at = timezone.now()
self.escalated_to = escalated_to_user
self.save()
@classmethod
def get_overdue_documentation(cls, tenant=None, senior_therapist=None):
"""
Get all overdue documentation.
Args:
tenant: Optional tenant filter
senior_therapist: Optional senior therapist filter
Returns:
QuerySet: Overdue documentation trackers
"""
queryset = cls.objects.filter(
status__in=[cls.Status.OVERDUE, cls.Status.ESCALATED]
)
if tenant:
queryset = queryset.filter(tenant=tenant)
if senior_therapist:
queryset = queryset.filter(senior_therapist=senior_therapist)
return queryset.order_by('-days_overdue')
@classmethod
def get_pending_alerts(cls, tenant=None):
"""
Get documentation that needs alerts sent (>5 days overdue, no recent alert).
Args:
tenant: Optional tenant filter
Returns:
QuerySet: Documentation needing alerts
"""
from datetime import date
queryset = cls.objects.filter(
status__in=[cls.Status.OVERDUE, cls.Status.ESCALATED],
days_overdue__gte=5
)
# Only send alerts once per day
yesterday = timezone.now() - timedelta(days=1)
queryset = queryset.filter(
Q(last_alert_at__isnull=True) | Q(last_alert_at__lt=yesterday)
)
if tenant:
queryset = queryset.filter(tenant=tenant)
return queryset
@classmethod
def create_tracker_for_session(cls, session, assigned_to, senior_therapist, tenant):
"""
Create a documentation tracker for a therapy session.
Args:
session: Session instance (OT, ABA, SLP, etc.)
assigned_to: User who should complete the documentation
senior_therapist: Senior therapist to notify
tenant: Tenant instance
Returns:
DocumentationDelayTracker instance
"""
from datetime import date
# Documentation due 2 working days after session
due_date = date.today() + timedelta(days=2)
# Adjust for weekends
while due_date.weekday() in [4, 5]: # Friday, Saturday
due_date += timedelta(days=1)
tracker = cls.objects.create(
document_type=cls.DocumentType.SESSION_NOTE,
document_id=session.id,
assigned_to=assigned_to,
senior_therapist=senior_therapist,
tenant=tenant,
due_date=due_date
)
return tracker