283 lines
8.8 KiB
Python
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
|