""" KPI Report Models - Monthly automated reports based on MOH requirements This module implements KPI reports that match the Excel-style templates: - 72H Resolution Rate (MOH-2) - Patient Experience Score (MOH-1) - Overall Satisfaction with Resolution (MOH-3) - N-PAD-001 Resolution Rate - Response Rate (Dep-KPI-4) - Activation Within 2 Hours (KPI-6) - Unactivated Filled Complaints Rate (KPI-7) """ from django.db import models from django.utils.translation import gettext_lazy as _ from apps.core.models import TimeStampedModel, UUIDModel class KPIReportType(models.TextChoices): """KPI report types matching MOH and internal requirements""" RESOLUTION_72H = "resolution_72h", _("72-Hour Resolution Rate (MOH-2)") PATIENT_EXPERIENCE = "patient_experience", _("Patient Experience Score (MOH-1)") SATISFACTION_RESOLUTION = "satisfaction_resolution", _("Overall Satisfaction with Resolution (MOH-3)") N_PAD_001 = "n_pad_001", _("Resolution to Patient Complaints (N-PAD-001)") RESPONSE_RATE = "response_rate", _("Department Response Rate (Dep-KPI-4)") ACTIVATION_2H = "activation_2h", _("Complaint Activation Within 2 Hours (KPI-6)") UNACTIVATED = "unactivated", _("Unactivated Filled Complaints Rate (KPI-7)") class KPIReportStatus(models.TextChoices): """Status of KPI report generation""" PENDING = "pending", _("Pending") GENERATING = "generating", _("Generating") COMPLETED = "completed", _("Completed") FAILED = "failed", _("Failed") class KPIReport(UUIDModel, TimeStampedModel): """ KPI Report - Monthly automated report for a specific KPI type Each report represents one month of data for a specific KPI, matching the Excel-style table format from the reference templates. """ report_type = models.CharField( max_length=50, choices=KPIReportType.choices, db_index=True, help_text=_("Type of KPI report") ) # Organization scope hospital = models.ForeignKey( "organizations.Hospital", on_delete=models.CASCADE, related_name="kpi_reports", help_text=_("Hospital this report belongs to") ) # Reporting period year = models.IntegerField(db_index=True) month = models.IntegerField(db_index=True) # Report metadata report_date = models.DateField( help_text=_("Date the report was generated") ) status = models.CharField( max_length=20, choices=KPIReportStatus.choices, default=KPIReportStatus.PENDING, db_index=True ) generated_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="generated_kpi_reports", help_text=_("User who generated the report (null for automated)") ) generated_at = models.DateTimeField(null=True, blank=True) # Report configuration metadata target_percentage = models.DecimalField( max_digits=5, decimal_places=2, default=95.00, help_text=_("Target percentage for this KPI") ) # Report metadata (category, type, risk, etc.) category = models.CharField( max_length=50, default="Organizational", help_text=_("Report category (e.g., Organizational, Clinical)") ) kpi_type = models.CharField( max_length=50, default="Outcome", help_text=_("KPI type (e.g., Outcome, Process, Structure)") ) risk_level = models.CharField( max_length=20, default="High", choices=[ ("High", "High"), ("Medium", "Medium"), ("Low", "Low"), ], help_text=_("Risk level for this KPI") ) data_collection_method = models.CharField( max_length=50, default="Retrospective", help_text=_("Data collection method") ) data_collection_frequency = models.CharField( max_length=50, default="Monthly", help_text=_("How often data is collected") ) reporting_frequency = models.CharField( max_length=50, default="Monthly", help_text=_("How often report is generated") ) dimension = models.CharField( max_length=50, default="Efficiency", help_text=_("KPI dimension (e.g., Efficiency, Quality, Safety)") ) collector_name = models.CharField( max_length=200, blank=True, help_text=_("Name of data collector") ) analyzer_name = models.CharField( max_length=200, blank=True, help_text=_("Name of data analyzer") ) # Summary metrics total_numerator = models.IntegerField( default=0, help_text=_("Total count of successful outcomes") ) total_denominator = models.IntegerField( default=0, help_text=_("Total count of all cases") ) overall_result = models.DecimalField( max_digits=6, decimal_places=2, default=0.00, help_text=_("Overall percentage result") ) # Error tracking error_message = models.TextField(blank=True) class Meta: ordering = ["-year", "-month", "report_type"] unique_together = [["report_type", "hospital", "year", "month"]] indexes = [ models.Index(fields=["report_type", "-year", "-month"]), models.Index(fields=["hospital", "-year", "-month"]), models.Index(fields=["status", "-created_at"]), ] verbose_name = "KPI Report" verbose_name_plural = "KPI Reports" def __str__(self): return f"{self.get_report_type_display()} - {self.year}/{self.month:02d} - {self.hospital.name}" @property def report_period_display(self): """Get human-readable report period""" month_names = [ "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] return f"{month_names[self.month]} {self.year}" @property def kpi_id(self): """Get KPI ID based on report type""" mapping = { KPIReportType.RESOLUTION_72H: "MOH-2", KPIReportType.PATIENT_EXPERIENCE: "MOH-1", KPIReportType.SATISFACTION_RESOLUTION: "MOH-3", KPIReportType.N_PAD_001: "N-PAD-001", KPIReportType.RESPONSE_RATE: "Dep-KPI-4", KPIReportType.ACTIVATION_2H: "KPI-6", KPIReportType.UNACTIVATED: "KPI-7", } return mapping.get(self.report_type, "KPI") @property def indicator_title(self): """Get indicator title based on report type""" titles = { KPIReportType.RESOLUTION_72H: "72-Hour Complaint Resolution Rate", KPIReportType.PATIENT_EXPERIENCE: "Patient Experience Score", KPIReportType.SATISFACTION_RESOLUTION: "Overall Satisfaction with Complaint Resolution", KPIReportType.N_PAD_001: "Resolution to Patient Complaints", KPIReportType.RESPONSE_RATE: "Department Response Rate (48h)", KPIReportType.ACTIVATION_2H: "Complaint Activation Within 2 Hours", KPIReportType.UNACTIVATED: "Unactivated Filled Complaints Rate", } return titles.get(self.report_type, "KPI Report") @property def numerator_label(self): """Get label for numerator based on report type""" labels = { KPIReportType.RESOLUTION_72H: "Resolved ≤72h", KPIReportType.PATIENT_EXPERIENCE: "Positive Responses", KPIReportType.SATISFACTION_RESOLUTION: "Satisfied Responses", KPIReportType.N_PAD_001: "Resolved Complaints", KPIReportType.RESPONSE_RATE: "Responded Within 48h", KPIReportType.ACTIVATION_2H: "Activated Within 2h", KPIReportType.UNACTIVATED: "Unactivated Complaints", } return labels.get(self.report_type, "Numerator") @property def denominator_label(self): """Get label for denominator based on report type""" labels = { KPIReportType.RESOLUTION_72H: "Total complaints", KPIReportType.PATIENT_EXPERIENCE: "Total responses", KPIReportType.SATISFACTION_RESOLUTION: "Total responses", KPIReportType.N_PAD_001: "Total complaints", KPIReportType.RESPONSE_RATE: "Total complaints", KPIReportType.ACTIVATION_2H: "Total complaints", KPIReportType.UNACTIVATED: "Total filled complaints", } return labels.get(self.report_type, "Denominator") class KPIReportMonthlyData(UUIDModel, TimeStampedModel): """ Monthly breakdown data for a KPI report Stores the Jan-Dec + TOTAL values shown in the Excel-style table. This allows for trend analysis and historical comparison. """ kpi_report = models.ForeignKey( KPIReport, on_delete=models.CASCADE, related_name="monthly_data" ) # Month (1-12) - 0 represents the TOTAL row month = models.IntegerField( db_index=True, help_text=_("Month number (1-12), 0 for TOTAL") ) # Values for this month numerator = models.IntegerField( default=0, help_text=_("Count of successful outcomes") ) denominator = models.IntegerField( default=0, help_text=_("Count of all cases") ) percentage = models.DecimalField( max_digits=6, decimal_places=2, default=0.00, help_text=_("Calculated percentage") ) # Status indicators is_below_target = models.BooleanField( default=False, help_text=_("Whether this month is below target") ) # Additional metadata for this month details = models.JSONField( default=dict, blank=True, help_text=_("Additional breakdown data (e.g., by source, department)") ) class Meta: ordering = ["month"] unique_together = [["kpi_report", "month"]] indexes = [ models.Index(fields=["kpi_report", "month"]), ] verbose_name = "KPI Monthly Data" verbose_name_plural = "KPI Monthly Data" def __str__(self): month_name = "TOTAL" if self.month == 0 else [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ][self.month - 1] return f"{self.kpi_report} - {month_name}: {self.percentage}%" def calculate_percentage(self): """Calculate percentage from numerator and denominator""" if self.denominator > 0: self.percentage = (self.numerator / self.denominator) * 100 else: self.percentage = 0 return self.percentage class KPIReportDepartmentBreakdown(UUIDModel, TimeStampedModel): """ Department-level breakdown for KPI reports Stores metrics for each department to show in the department grid section of the report (Medical, Nursing, Admin, Support Services). """ kpi_report = models.ForeignKey( KPIReport, on_delete=models.CASCADE, related_name="department_breakdowns" ) department_category = models.CharField( max_length=50, choices=[ ("medical", "Medical Department"), ("nursing", "Nursing Department"), ("admin", "Non-Medical / Admin"), ("support", "Support Services"), ], help_text=_("Category of department") ) # Department-specific metrics complaint_count = models.IntegerField(default=0) resolved_count = models.IntegerField(default=0) avg_resolution_days = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True ) # Top complaints/areas (stored as text for display) top_areas = models.TextField( blank=True, help_text=_("Top complaint areas or notes (newline-separated)") ) # JSON field for flexible department-specific data details = models.JSONField(default=dict, blank=True) class Meta: ordering = ["department_category"] unique_together = [["kpi_report", "department_category"]] verbose_name = "KPI Department Breakdown" verbose_name_plural = "KPI Department Breakdowns" def __str__(self): return f"{self.kpi_report} - {self.get_department_category_display()}" class KPIReportSourceBreakdown(UUIDModel, TimeStampedModel): """ Complaint source breakdown for KPI reports Stores percentage distribution of complaints by source (Patient, Family, Staff, MOH, CHI, etc.) """ kpi_report = models.ForeignKey( KPIReport, on_delete=models.CASCADE, related_name="source_breakdowns" ) source_name = models.CharField(max_length=100) complaint_count = models.IntegerField(default=0) percentage = models.DecimalField(max_digits=5, decimal_places=2, default=0.00) class Meta: ordering = ["-complaint_count"] verbose_name = "KPI Source Breakdown" verbose_name_plural = "KPI Source Breakdowns" def __str__(self): return f"{self.kpi_report} - {self.source_name}: {self.percentage}%"