""" PX Action Center models - Action tracking with SLA and escalation This module implements the PX Action Center that: - Creates actions automatically from negative feedback - Tracks SLA deadlines - Sends reminders before due date - Escalates overdue actions - Manages approval workflow - Tracks evidence and attachments """ from datetime import timedelta from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone from apps.core.models import PriorityChoices, SeverityChoices, TimeStampedModel, UUIDModel class ActionStatus(models.TextChoices): """PX Action status choices""" OPEN = 'open', 'Open' IN_PROGRESS = 'in_progress', 'In Progress' PENDING_APPROVAL = 'pending_approval', 'Pending Approval' APPROVED = 'approved', 'Approved' CLOSED = 'closed', 'Closed' CANCELLED = 'cancelled', 'Cancelled' class ActionSource(models.TextChoices): """PX Action source choices""" SURVEY = 'survey', 'Negative Survey' COMPLAINT = 'complaint', 'Complaint' COMPLAINT_RESOLUTION = 'complaint_resolution', 'Negative Complaint Resolution' SOCIAL_MEDIA = 'social_media', 'Social Media' CALL_CENTER = 'call_center', 'Call Center' KPI = 'kpi', 'KPI Decline' MANUAL = 'manual', 'Manual' class PXAction(UUIDModel, TimeStampedModel): """ PX Action - tracks improvement actions with SLA. Actions are created automatically from: - Negative stage survey scores - Negative complaint resolution satisfaction - Complaints (if configured) - Negative social media sentiment - Low call center ratings - KPI declines Workflow: 1. OPEN - Action created 2. IN_PROGRESS - Being worked on 3. PENDING_APPROVAL - Awaiting PX approval 4. APPROVED - Approved by PX 5. CLOSED - Completed """ # Source information (generic foreign key) source_type = models.CharField( max_length=50, choices=ActionSource.choices, db_index=True ) content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, null=True, blank=True ) object_id = models.UUIDField(null=True, blank=True) source_object = GenericForeignKey('content_type', 'object_id') # Action details title = models.CharField(max_length=500) description = models.TextField() # Organization hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='px_actions' ) department = models.ForeignKey( 'organizations.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='px_actions' ) # Classification category = models.CharField( max_length=100, choices=[ ('clinical_quality', 'Clinical Quality'), ('patient_safety', 'Patient Safety'), ('service_quality', 'Service Quality'), ('staff_behavior', 'Staff Behavior'), ('facility', 'Facility & Environment'), ('process_improvement', 'Process Improvement'), ('other', 'Other'), ], db_index=True ) priority = models.CharField( max_length=20, choices=PriorityChoices.choices, default=PriorityChoices.MEDIUM, db_index=True ) severity = models.CharField( max_length=20, choices=SeverityChoices.choices, default=SeverityChoices.MEDIUM, db_index=True ) # Status and workflow status = models.CharField( max_length=20, choices=ActionStatus.choices, default=ActionStatus.OPEN, db_index=True ) # Assignment assigned_to = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_actions' ) assigned_at = models.DateTimeField(null=True, blank=True) # SLA tracking due_at = models.DateTimeField( db_index=True, help_text="SLA deadline" ) is_overdue = models.BooleanField(default=False, db_index=True) reminder_sent_at = models.DateTimeField(null=True, blank=True) escalated_at = models.DateTimeField(null=True, blank=True) escalation_level = models.IntegerField( default=0, help_text="Number of times escalated" ) # Approval requires_approval = models.BooleanField( default=True, help_text="Requires PX Admin approval before closure" ) approved_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_actions' ) approved_at = models.DateTimeField(null=True, blank=True) # Closure closed_at = models.DateTimeField(null=True, blank=True) closed_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='closed_actions' ) # Action plan and outcome action_plan = models.TextField(blank=True) outcome = models.TextField(blank=True) # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['status', '-created_at']), models.Index(fields=['hospital', 'status', '-created_at']), models.Index(fields=['is_overdue', 'status']), models.Index(fields=['due_at', 'status']), models.Index(fields=['source_type', '-created_at']), ] def __str__(self): return f"{self.title} ({self.status})" def save(self, *args, **kwargs): """Calculate SLA due date on creation""" if not self.due_at: self.due_at = self.calculate_sla_due_date() super().save(*args, **kwargs) def calculate_sla_due_date(self): """Calculate SLA due date based on severity""" sla_hours = settings.SLA_DEFAULTS['action'].get( self.severity, settings.SLA_DEFAULTS['action']['medium'] ) return timezone.now() + timedelta(hours=sla_hours) def check_overdue(self): """Check if action is overdue""" if self.status in [ActionStatus.CLOSED, ActionStatus.CANCELLED]: return False if timezone.now() > self.due_at: if not self.is_overdue: self.is_overdue = True self.save(update_fields=['is_overdue']) return True return False class PXActionLog(UUIDModel, TimeStampedModel): """ Action log entry - tracks updates and discussions. """ action = models.ForeignKey( PXAction, on_delete=models.CASCADE, related_name='logs' ) log_type = models.CharField( max_length=50, choices=[ ('status_change', 'Status Change'), ('assignment', 'Assignment'), ('escalation', 'Escalation'), ('note', 'Note'), ('evidence', 'Evidence Added'), ('approval', 'Approval'), ('sla_reminder', 'SLA Reminder'), ], db_index=True ) message = models.TextField() created_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, related_name='action_logs' ) # Status change tracking old_status = models.CharField(max_length=20, blank=True) new_status = models.CharField(max_length=20, blank=True) metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['action', '-created_at']), ] def __str__(self): return f"{self.action} - {self.log_type} - {self.created_at.strftime('%Y-%m-%d %H:%M')}" class PXActionAttachment(UUIDModel, TimeStampedModel): """Action attachment - evidence, documents, etc.""" action = models.ForeignKey( PXAction, on_delete=models.CASCADE, related_name='attachments' ) file = models.FileField(upload_to='actions/%Y/%m/%d/') filename = models.CharField(max_length=500) file_type = models.CharField(max_length=100, blank=True) file_size = models.IntegerField(help_text="File size in bytes") uploaded_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, related_name='action_attachments' ) description = models.TextField(blank=True) is_evidence = models.BooleanField( default=False, help_text="Mark as evidence for closure" ) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.action} - {self.filename}" class PXActionSLAConfig(UUIDModel, TimeStampedModel): """ SLA configuration for PX Actions. Allows per-hospital or per-department SLA customization. """ name = models.CharField(max_length=200) # Scope hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, null=True, blank=True, related_name='action_sla_configs' ) department = models.ForeignKey( 'organizations.Department', on_delete=models.CASCADE, null=True, blank=True, related_name='action_sla_configs' ) # SLA durations (in hours) by severity critical_hours = models.IntegerField(default=24) high_hours = models.IntegerField(default=48) medium_hours = models.IntegerField(default=72) low_hours = models.IntegerField(default=120) # Reminder configuration reminder_hours_before = models.IntegerField( default=4, help_text="Send reminder X hours before due" ) # Escalation configuration auto_escalate = models.BooleanField( default=True, help_text="Automatically escalate when overdue" ) escalation_delay_hours = models.IntegerField( default=2, help_text="Hours after overdue before escalation" ) is_active = models.BooleanField(default=True) class Meta: ordering = ['hospital', 'name'] def __str__(self): scope = self.hospital.name if self.hospital else "Global" return f"{self.name} ({scope})" class RoutingRule(UUIDModel, TimeStampedModel): """ Routing rule - determines where actions are assigned. Rules can be based on: - Source type - Category - Severity - Hospital/Department """ name = models.CharField(max_length=200) description = models.TextField(blank=True) # Conditions source_type = models.CharField( max_length=50, choices=ActionSource.choices, blank=True ) category = models.CharField(max_length=100, blank=True) severity = models.CharField( max_length=20, choices=SeverityChoices.choices, blank=True ) hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, null=True, blank=True, related_name='routing_rules' ) department = models.ForeignKey( 'organizations.Department', on_delete=models.CASCADE, null=True, blank=True, related_name='routing_rules' ) # Routing target assign_to_role = models.CharField( max_length=50, blank=True, help_text="Role to assign to (e.g., 'PX Coordinator')" ) assign_to_user = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='routing_rules' ) assign_to_department = models.ForeignKey( 'organizations.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='routing_target_rules', help_text="Department to assign to" ) # Priority priority = models.IntegerField( default=0, help_text="Higher priority rules are evaluated first" ) is_active = models.BooleanField(default=True) class Meta: ordering = ['-priority', 'name'] def __str__(self): return self.name