1859 lines
78 KiB
Python
1859 lines
78 KiB
Python
"""
|
|
KPI Report Calculation Service
|
|
|
|
This service calculates KPI metrics for monthly reports based on
|
|
the complaint and survey data in the system.
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
from typing import Dict, List, Optional
|
|
|
|
from django.db.models import Avg, Count, F, Q
|
|
from django.utils import timezone
|
|
|
|
from apps.complaints.models import Complaint, ComplaintStatus, ComplaintSource
|
|
from apps.organizations.models import Department
|
|
from apps.surveys.models import SurveyInstance, SurveyStatus, SurveyTemplate
|
|
|
|
from .kpi_models import (
|
|
KPIReport,
|
|
KPIReportDepartmentBreakdown,
|
|
KPIReportMonthlyData,
|
|
KPIReportSourceBreakdown,
|
|
KPIReportStatus,
|
|
KPIReportType,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class KPICalculationService:
|
|
"""
|
|
Service for calculating KPI report metrics
|
|
|
|
Handles the complex calculations for each KPI type:
|
|
- 72H Resolution Rate
|
|
- Patient Experience Score
|
|
- Satisfaction with Resolution
|
|
- Response Rate
|
|
- Activation Time
|
|
- Unactivated Rate
|
|
"""
|
|
|
|
@classmethod
|
|
def generate_monthly_report(
|
|
cls,
|
|
report_type: str,
|
|
hospital,
|
|
year: int,
|
|
month: int,
|
|
generated_by=None
|
|
) -> KPIReport:
|
|
"""
|
|
Generate a complete monthly KPI report
|
|
|
|
Args:
|
|
report_type: Type of KPI report (from KPIReportType)
|
|
hospital: Hospital instance
|
|
year: Report year
|
|
month: Report month (1-12)
|
|
generated_by: User who generated the report (optional)
|
|
|
|
Returns:
|
|
KPIReport instance
|
|
"""
|
|
# Get or create the report
|
|
report, created = KPIReport.objects.get_or_create(
|
|
report_type=report_type,
|
|
hospital=hospital,
|
|
year=year,
|
|
month=month,
|
|
defaults={
|
|
"report_date": timezone.now().date(),
|
|
"status": KPIReportStatus.PENDING,
|
|
"generated_by": generated_by,
|
|
}
|
|
)
|
|
|
|
if not created and report.status == KPIReportStatus.COMPLETED:
|
|
# Report already exists and is complete - return it
|
|
return report
|
|
|
|
# Update status to generating
|
|
report.status = KPIReportStatus.GENERATING
|
|
report.save()
|
|
|
|
try:
|
|
# Calculate based on report type
|
|
if report_type == KPIReportType.RESOLUTION_72H:
|
|
cls._calculate_72h_resolution(report)
|
|
elif report_type == KPIReportType.PATIENT_EXPERIENCE:
|
|
cls._calculate_patient_experience(report)
|
|
elif report_type == KPIReportType.SATISFACTION_RESOLUTION:
|
|
cls._calculate_satisfaction_resolution(report)
|
|
elif report_type == KPIReportType.N_PAD_001:
|
|
cls._calculate_n_pad_001(report)
|
|
elif report_type == KPIReportType.RESPONSE_RATE:
|
|
cls._calculate_response_rate(report)
|
|
elif report_type == KPIReportType.ACTIVATION_2H:
|
|
cls._calculate_activation_2h(report)
|
|
elif report_type == KPIReportType.UNACTIVATED:
|
|
cls._calculate_unactivated(report)
|
|
|
|
# Mark as completed
|
|
report.status = KPIReportStatus.COMPLETED
|
|
report.generated_at = timezone.now()
|
|
report.save()
|
|
|
|
# Generate AI analysis for supported report types
|
|
supported_types = [
|
|
KPIReportType.RESOLUTION_72H,
|
|
KPIReportType.N_PAD_001,
|
|
KPIReportType.SATISFACTION_RESOLUTION,
|
|
KPIReportType.RESPONSE_RATE,
|
|
KPIReportType.ACTIVATION_2H,
|
|
KPIReportType.UNACTIVATED
|
|
]
|
|
if report_type in supported_types:
|
|
try:
|
|
cls.generate_ai_analysis(report)
|
|
logger.info(f"AI analysis generated for KPI report {report.id}")
|
|
except Exception as ai_error:
|
|
logger.warning(f"Failed to generate AI analysis for report {report.id}: {ai_error}")
|
|
|
|
logger.info(f"KPI Report {report.id} generated successfully")
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating KPI report {report.id}")
|
|
report.status = KPIReportStatus.FAILED
|
|
report.error_message = str(e)
|
|
report.save()
|
|
raise
|
|
|
|
return report
|
|
|
|
@classmethod
|
|
def _calculate_72h_resolution(cls, report: KPIReport):
|
|
"""Calculate 72-Hour Resolution Rate (MOH-2)"""
|
|
# Get date range for the report period
|
|
start_date = datetime(report.year, report.month, 1)
|
|
if report.month == 12:
|
|
end_date = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
end_date = datetime(report.year, report.month + 1, 1)
|
|
|
|
# Get all months data for YTD (year to date)
|
|
year_start = datetime(report.year, 1, 1)
|
|
|
|
# Calculate for each month
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get complaints for this month
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=month_start,
|
|
created_at__lt=month_end,
|
|
complaint_type="complaint" # Only actual complaints
|
|
)
|
|
|
|
# Count total complaints
|
|
denominator = complaints.count()
|
|
|
|
# Count resolved within 72 hours
|
|
numerator = 0
|
|
for complaint in complaints:
|
|
if complaint.resolved_at and complaint.created_at:
|
|
resolution_time = complaint.resolved_at - complaint.created_at
|
|
if resolution_time.total_seconds() <= 72 * 3600: # 72 hours
|
|
numerator += 1
|
|
|
|
# Create or update monthly data
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
monthly_data.is_below_target = monthly_data.percentage < report.target_percentage
|
|
|
|
# Store source breakdown in details
|
|
source_data = cls._get_source_breakdown(complaints)
|
|
monthly_data.details = {"source_breakdown": source_data}
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
# Update report totals
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
# Create source breakdown for pie chart
|
|
all_complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=year_start,
|
|
created_at__lt=end_date,
|
|
complaint_type="complaint"
|
|
)
|
|
cls._create_source_breakdowns(report, all_complaints)
|
|
|
|
# Create department breakdown
|
|
cls._create_department_breakdown(report, all_complaints)
|
|
|
|
@classmethod
|
|
def _calculate_patient_experience(cls, report: KPIReport):
|
|
"""Calculate Patient Experience Score (MOH-1)"""
|
|
# Get date range
|
|
year_start = datetime(report.year, 1, 1)
|
|
start_date = datetime(report.year, report.month, 1)
|
|
if report.month == 12:
|
|
end_date = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
end_date = datetime(report.year, report.month + 1, 1)
|
|
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get completed surveys for patient experience
|
|
surveys = SurveyInstance.objects.filter(
|
|
survey_template__hospital=report.hospital,
|
|
status=SurveyStatus.COMPLETED,
|
|
completed_at__gte=month_start,
|
|
completed_at__lt=month_end,
|
|
survey_template__survey_type__in=["stage", "general"]
|
|
)
|
|
|
|
denominator = surveys.count()
|
|
|
|
# Count positive responses (score >= 4 out of 5)
|
|
numerator = surveys.filter(total_score__gte=4).count()
|
|
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
monthly_data.is_below_target = monthly_data.percentage < report.target_percentage
|
|
|
|
# Store average score
|
|
avg_score = surveys.aggregate(avg=Avg('total_score'))['avg'] or 0
|
|
monthly_data.details = {"avg_score": round(avg_score, 2)}
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
@classmethod
|
|
def _calculate_satisfaction_resolution(cls, report: KPIReport):
|
|
"""Calculate Overall Satisfaction with Resolution (MOH-3)"""
|
|
year_start = datetime(report.year, 1, 1)
|
|
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get complaint resolution surveys
|
|
surveys = SurveyInstance.objects.filter(
|
|
survey_template__hospital=report.hospital,
|
|
status=SurveyStatus.COMPLETED,
|
|
completed_at__gte=month_start,
|
|
completed_at__lt=month_end,
|
|
survey_template__survey_type="complaint_resolution"
|
|
)
|
|
|
|
denominator = surveys.count()
|
|
# Satisfied = score >= 4
|
|
numerator = surveys.filter(total_score__gte=4).count()
|
|
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
monthly_data.is_below_target = monthly_data.percentage < report.target_percentage
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
@classmethod
|
|
def _calculate_n_pad_001(cls, report: KPIReport):
|
|
"""Calculate N-PAD-001 Resolution Rate"""
|
|
year_start = datetime(report.year, 1, 1)
|
|
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get complaints
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=month_start,
|
|
created_at__lt=month_end,
|
|
complaint_type="complaint"
|
|
)
|
|
|
|
denominator = complaints.count()
|
|
# Resolved includes closed and resolved statuses
|
|
numerator = complaints.filter(
|
|
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED]
|
|
).count()
|
|
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
monthly_data.is_below_target = monthly_data.percentage < report.target_percentage
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
@classmethod
|
|
def _calculate_response_rate(cls, report: KPIReport):
|
|
"""Calculate Department Response Rate (48h)"""
|
|
year_start = datetime(report.year, 1, 1)
|
|
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get complaints that received a response
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=month_start,
|
|
created_at__lt=month_end,
|
|
complaint_type="complaint"
|
|
)
|
|
|
|
denominator = complaints.count()
|
|
|
|
# Count complaints with response within 48h
|
|
numerator = 0
|
|
for complaint in complaints:
|
|
first_update = complaint.updates.order_by('created_at').first()
|
|
if first_update and complaint.created_at:
|
|
response_time = first_update.created_at - complaint.created_at
|
|
if response_time.total_seconds() <= 48 * 3600:
|
|
numerator += 1
|
|
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
monthly_data.is_below_target = monthly_data.percentage < report.target_percentage
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
@classmethod
|
|
def _calculate_activation_2h(cls, report: KPIReport):
|
|
"""Calculate Complaint Activation Within 2 Hours"""
|
|
year_start = datetime(report.year, 1, 1)
|
|
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get complaints with assigned_to (activated)
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=month_start,
|
|
created_at__lt=month_end,
|
|
complaint_type="complaint"
|
|
)
|
|
|
|
denominator = complaints.count()
|
|
|
|
# Count activated within 2 hours
|
|
numerator = 0
|
|
for complaint in complaints:
|
|
if complaint.assigned_at and complaint.created_at:
|
|
activation_time = complaint.assigned_at - complaint.created_at
|
|
if activation_time.total_seconds() <= 2 * 3600:
|
|
numerator += 1
|
|
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
monthly_data.is_below_target = monthly_data.percentage < report.target_percentage
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
@classmethod
|
|
def _calculate_unactivated(cls, report: KPIReport):
|
|
"""Calculate Unactivated Filled Complaints Rate"""
|
|
year_start = datetime(report.year, 1, 1)
|
|
|
|
total_numerator = 0
|
|
total_denominator = 0
|
|
|
|
for month in range(1, 13):
|
|
month_start = datetime(report.year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(report.year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(report.year, month + 1, 1)
|
|
|
|
# Get all complaints
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=month_start,
|
|
created_at__lt=month_end,
|
|
complaint_type="complaint"
|
|
)
|
|
|
|
denominator = complaints.count()
|
|
# Unactivated = no assigned_to
|
|
numerator = complaints.filter(assigned_to__isnull=True).count()
|
|
|
|
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
|
|
kpi_report=report,
|
|
month=month,
|
|
defaults={
|
|
"numerator": numerator,
|
|
"denominator": denominator,
|
|
}
|
|
)
|
|
monthly_data.numerator = numerator
|
|
monthly_data.denominator = denominator
|
|
monthly_data.calculate_percentage()
|
|
# Note: For unactivated, HIGHER is WORSE, so below target = above threshold
|
|
monthly_data.is_below_target = monthly_data.percentage > (100 - report.target_percentage)
|
|
monthly_data.save()
|
|
|
|
total_numerator += numerator
|
|
total_denominator += denominator
|
|
|
|
report.total_numerator = total_numerator
|
|
report.total_denominator = total_denominator
|
|
if total_denominator > 0:
|
|
report.overall_result = Decimal(str((total_numerator / total_denominator) * 100))
|
|
report.save()
|
|
|
|
@classmethod
|
|
def _get_source_breakdown(cls, complaints) -> Dict[str, int]:
|
|
"""Get breakdown of complaints by source"""
|
|
sources = {}
|
|
for complaint in complaints:
|
|
source_name = complaint.source.name_en if complaint.source else "Other"
|
|
sources[source_name] = sources.get(source_name, 0) + 1
|
|
return sources
|
|
|
|
@classmethod
|
|
def _create_source_breakdowns(cls, report: KPIReport, complaints):
|
|
"""Create source breakdown records for pie chart"""
|
|
# Delete existing
|
|
report.source_breakdowns.all().delete()
|
|
|
|
# Count by source
|
|
source_counts = {}
|
|
total = complaints.count()
|
|
|
|
for complaint in complaints:
|
|
source_name = complaint.source.name_en if complaint.source else "Other"
|
|
source_counts[source_name] = source_counts.get(source_name, 0) + 1
|
|
|
|
# Create records
|
|
for source_name, count in source_counts.items():
|
|
percentage = (count / total * 100) if total > 0 else 0
|
|
KPIReportSourceBreakdown.objects.create(
|
|
kpi_report=report,
|
|
source_name=source_name,
|
|
complaint_count=count,
|
|
percentage=Decimal(str(percentage))
|
|
)
|
|
|
|
@classmethod
|
|
def _create_department_breakdown(cls, report: KPIReport, complaints):
|
|
"""Create department breakdown records"""
|
|
# Delete existing
|
|
report.department_breakdowns.all().delete()
|
|
|
|
# Categorize departments
|
|
department_categories = {
|
|
"medical": ["Medical", "Surgery", "Cardiology", "Orthopedics", "Pediatrics", "Obstetrics", "Gynecology"],
|
|
"nursing": ["Nursing", "ICU", "ER", "OR"],
|
|
"admin": ["Administration", "HR", "Finance", "IT", "Reception"],
|
|
"support": ["Housekeeping", "Maintenance", "Security", "Cafeteria", "Transport"],
|
|
}
|
|
|
|
for category, keywords in department_categories.items():
|
|
# Find departments matching this category
|
|
dept_complaints = complaints.filter(
|
|
department__name__icontains=keywords[0]
|
|
)
|
|
for keyword in keywords[1:]:
|
|
dept_complaints = dept_complaints | complaints.filter(
|
|
department__name__icontains=keyword
|
|
)
|
|
|
|
complaint_count = dept_complaints.count()
|
|
resolved_count = dept_complaints.filter(
|
|
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED]
|
|
).count()
|
|
|
|
# Calculate average resolution days
|
|
avg_days = None
|
|
resolved_complaints = dept_complaints.filter(resolved_at__isnull=False)
|
|
if resolved_complaints.exists():
|
|
total_days = 0
|
|
for c in resolved_complaints:
|
|
days = (c.resolved_at - c.created_at).total_seconds() / (24 * 3600)
|
|
total_days += days
|
|
avg_days = Decimal(str(total_days / resolved_complaints.count()))
|
|
|
|
# Get top areas (subcategories)
|
|
top_areas_list = []
|
|
for c in dept_complaints[:10]:
|
|
if c.category:
|
|
top_areas_list.append(c.category.name_en)
|
|
top_areas = "\n".join(list(set(top_areas_list))[:5]) if top_areas_list else ""
|
|
|
|
KPIReportDepartmentBreakdown.objects.create(
|
|
kpi_report=report,
|
|
department_category=category,
|
|
complaint_count=complaint_count,
|
|
resolved_count=resolved_count,
|
|
avg_resolution_days=avg_days,
|
|
top_areas=top_areas
|
|
)
|
|
|
|
|
|
@classmethod
|
|
def generate_ai_analysis(cls, report: KPIReport) -> dict:
|
|
"""
|
|
Generate AI analysis for a KPI report.
|
|
|
|
Dispatches to specific analysis methods based on report type.
|
|
|
|
Args:
|
|
report: KPIReport instance
|
|
|
|
Returns:
|
|
Dictionary containing AI-generated analysis
|
|
"""
|
|
if report.report_type == KPIReportType.RESOLUTION_72H:
|
|
return cls._generate_72h_resolution_analysis(report)
|
|
elif report.report_type == KPIReportType.N_PAD_001:
|
|
return cls._generate_n_pad_001_analysis(report)
|
|
elif report.report_type == KPIReportType.SATISFACTION_RESOLUTION:
|
|
return cls._generate_satisfaction_resolution_analysis(report)
|
|
elif report.report_type == KPIReportType.RESPONSE_RATE:
|
|
return cls._generate_response_rate_analysis(report)
|
|
elif report.report_type == KPIReportType.ACTIVATION_2H:
|
|
return cls._generate_activation_2h_analysis(report)
|
|
elif report.report_type == KPIReportType.UNACTIVATED:
|
|
return cls._generate_unactivated_analysis(report)
|
|
else:
|
|
logger.warning(f"AI analysis not supported for report type: {report.report_type}")
|
|
return {'error': 'Analysis not supported for this report type'}
|
|
|
|
@classmethod
|
|
def _generate_72h_resolution_analysis(cls, report: KPIReport) -> dict:
|
|
"""Generate AI analysis for 72-Hour Resolution Rate report"""
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
# Get report data
|
|
overall_percentage = float(report.overall_result) if report.overall_result else 0
|
|
total_complaints = report.total_denominator
|
|
resolved_within_72h = report.total_numerator
|
|
|
|
# Get monthly data
|
|
monthly_data = report.monthly_data.all().order_by('month')
|
|
monthly_breakdown = []
|
|
for md in monthly_data:
|
|
if md.denominator > 0:
|
|
monthly_breakdown.append({
|
|
'month': md.month,
|
|
'percentage': float(md.percentage) if md.percentage else 0,
|
|
'resolved': md.numerator,
|
|
'total': md.denominator
|
|
})
|
|
|
|
# Get source breakdowns
|
|
source_breakdowns = report.source_breakdowns.all()
|
|
source_data = []
|
|
for sb in source_breakdowns:
|
|
source_data.append({
|
|
'source': sb.source_name,
|
|
'complaints': sb.complaint_count,
|
|
'percentage': float(sb.percentage) if sb.percentage else 0
|
|
})
|
|
|
|
# Get department breakdowns
|
|
dept_breakdowns = report.department_breakdowns.all()
|
|
dept_data = []
|
|
for db in dept_breakdowns:
|
|
dept_data.append({
|
|
'category': db.get_department_category_display(),
|
|
'complaints': db.complaint_count,
|
|
'resolved': db.resolved_count,
|
|
'avg_days': float(db.avg_resolution_days) if db.avg_resolution_days else None
|
|
})
|
|
|
|
# Calculate resolution time buckets
|
|
year_start = datetime(report.year, 1, 1)
|
|
year_end = datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
|
|
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=year_start,
|
|
created_at__lt=year_end,
|
|
complaint_type="complaint",
|
|
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED]
|
|
)
|
|
|
|
resolved_24h = resolved_48h = resolved_72h = resolved_over_72h = 0
|
|
dept_response_times = {}
|
|
|
|
for c in complaints:
|
|
if c.resolved_at and c.created_at:
|
|
hours = (c.resolved_at - c.created_at).total_seconds() / 3600
|
|
if hours <= 24:
|
|
resolved_24h += 1
|
|
elif hours <= 48:
|
|
resolved_48h += 1
|
|
elif hours <= 72:
|
|
resolved_72h += 1
|
|
else:
|
|
resolved_over_72h += 1
|
|
|
|
dept_name = c.department.name if c.department else "Unassigned"
|
|
if dept_name not in dept_response_times:
|
|
dept_response_times[dept_name] = []
|
|
dept_response_times[dept_name].append(hours / 24)
|
|
|
|
slow_departments = []
|
|
for dept, days_list in dept_response_times.items():
|
|
if days_list:
|
|
avg_days = sum(days_list) / len(days_list)
|
|
if avg_days > 2:
|
|
slow_departments.append({'name': dept, 'avg_days': round(avg_days, 1)})
|
|
slow_departments.sort(key=lambda x: x['avg_days'], reverse=True)
|
|
|
|
escalated_count = complaints.filter(updates__update_type='escalation').distinct().count()
|
|
closed_count = complaints.filter(status=ComplaintStatus.CLOSED).count()
|
|
|
|
target_percentage = float(report.target_percentage)
|
|
performance_status = "met" if overall_percentage >= target_percentage else "below target"
|
|
|
|
prompt = f"""Analyze this 72-Hour Complaint Resolution KPI report.
|
|
|
|
REPORT DATA:
|
|
- Report Period: {report.year}-{report.month:02d}
|
|
- Hospital: {report.hospital.name}
|
|
- Overall Resolution Rate (≤72h): {overall_percentage:.2f}%
|
|
- Target: {target_percentage:.2f}%
|
|
- Status: {performance_status}
|
|
- Total Complaints: {total_complaints}
|
|
- Resolved within 72h: {resolved_within_72h}
|
|
|
|
RESOLUTION TIME BREAKDOWN:
|
|
- Within 24h: {resolved_24h} ({(resolved_24h/total_complaints*100) if total_complaints else 0:.2f}%)
|
|
- Within 48h: {resolved_48h} ({(resolved_48h/total_complaints*100) if total_complaints else 0:.2f}%)
|
|
- Within 72h: {resolved_72h} ({(resolved_72h/total_complaints*100) if total_complaints else 0:.2f}%)
|
|
- After 72h: {resolved_over_72h} ({(resolved_over_72h/total_complaints*100) if total_complaints else 0:.2f}%)
|
|
|
|
MONTHLY TREND:
|
|
{chr(10).join([f"- Month {m['month']}: {m['percentage']:.2f}% ({m['resolved']}/{m['total']})" for m in monthly_breakdown])}
|
|
|
|
SOURCE BREAKDOWN:
|
|
{chr(10).join([f"- {s['source']}: {s['complaints']} ({s['percentage']:.2f}%)" for s in source_data])}
|
|
|
|
DEPARTMENT PERFORMANCE:
|
|
{chr(10).join([f"- {d['category']}: {d['complaints']} complaints, {d['resolved']} resolved" + (f", Avg {d['avg_days']:.1f} days" if d['avg_days'] else "") for d in dept_data])}
|
|
|
|
SLOW DEPARTMENTS:
|
|
{chr(10).join([f"- {d['name']}: {d['avg_days']} days" for d in slow_departments[:10]]) if slow_departments else "- None"}
|
|
|
|
ESCALATED: {escalated_count} | CLOSED: {closed_count}
|
|
|
|
Provide analysis in JSON format with: executive_summary, performance_analysis, key_findings, reasons_for_delays, resolution_time_analysis, department_analysis, source_analysis, recommendations."""
|
|
|
|
system_prompt = """You are a healthcare KPI analysis expert. Analyze 72-Hour Complaint Resolution data and provide:
|
|
1. Executive summary
|
|
2. Performance analysis vs target
|
|
3. Key findings
|
|
4. Root causes for delays
|
|
5. Department insights
|
|
6. Source insights (CHI, MOH, etc.)
|
|
7. Actionable recommendations
|
|
|
|
Be specific and use actual numbers."""
|
|
|
|
response = AIService.chat_completion(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
response_format="json_object",
|
|
temperature=0.3,
|
|
max_tokens=2000
|
|
)
|
|
|
|
import json
|
|
analysis = json.loads(response)
|
|
analysis['_metadata'] = {
|
|
'generated_at': timezone.now().isoformat(),
|
|
'report_id': str(report.id),
|
|
'report_type': report.report_type,
|
|
'hospital': report.hospital.name,
|
|
'year': report.year,
|
|
'month': report.month
|
|
}
|
|
|
|
report.ai_analysis = analysis
|
|
report.ai_analysis_generated_at = timezone.now()
|
|
report.save(update_fields=['ai_analysis', 'ai_analysis_generated_at'])
|
|
|
|
logger.info(f"AI analysis generated for 72h report {report.id}")
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating 72h analysis: {e}")
|
|
return {'error': str(e), 'executive_summary': 'Analysis failed'}
|
|
|
|
@classmethod
|
|
def _generate_n_pad_001_analysis(cls, report: KPIReport) -> dict:
|
|
"""Generate AI analysis for N-PAD-001 Resolution to Patient Complaints report"""
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
# Get report data
|
|
overall_percentage = float(report.overall_result) if report.overall_result else 0
|
|
total_complaints = report.total_denominator
|
|
resolved_complaints = report.total_numerator
|
|
|
|
# Get date range
|
|
year_start = datetime(report.year, 1, 1)
|
|
year_end = datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
|
|
|
|
# Query complaints for detailed analysis
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=year_start,
|
|
created_at__lt=year_end,
|
|
complaint_type="complaint"
|
|
).select_related('department', 'location', 'main_section', 'source')
|
|
|
|
# Count by status
|
|
closed_count = complaints.filter(status=ComplaintStatus.CLOSED).count()
|
|
resolved_count = complaints.filter(status=ComplaintStatus.RESOLVED).count()
|
|
escalated_count = complaints.filter(updates__update_type='escalation').distinct().count()
|
|
|
|
# Department breakdown
|
|
dept_counts = {}
|
|
for c in complaints:
|
|
dept_cat = 'Unassigned'
|
|
if c.department:
|
|
name = c.department.name.lower()
|
|
if any(k in name for k in ['medical', 'surgery', 'cardiology', 'orthopedics', 'pediatrics', 'obstetrics', 'gynecology', 'er', 'lab', 'icu']):
|
|
dept_cat = 'Medical'
|
|
elif any(k in name for k in ['nursing', 'nurse']):
|
|
dept_cat = 'Nursing'
|
|
elif any(k in name for k in ['admin', 'reception', 'manager', 'approval', 'report']):
|
|
dept_cat = 'Admin'
|
|
elif any(k in name for k in ['housekeeping', 'maintenance', 'security', 'cafeteria', 'transport']):
|
|
dept_cat = 'Support Services'
|
|
else:
|
|
dept_cat = c.department.name
|
|
|
|
if dept_cat not in dept_counts:
|
|
dept_counts[dept_cat] = {'closed': 0, 'escalated': 0, 'total': 0}
|
|
dept_counts[dept_cat]['total'] += 1
|
|
if c.status == ComplaintStatus.CLOSED:
|
|
dept_counts[dept_cat]['closed'] += 1
|
|
|
|
# Escalation reasons
|
|
escalated_complaints = complaints.filter(updates__update_type='escalation').distinct()
|
|
patient_dissatisfaction = 0
|
|
lack_of_response = 0
|
|
|
|
for c in escalated_complaints:
|
|
updates = c.updates.filter(update_type='escalation')
|
|
for u in updates:
|
|
message = u.message.lower() if u.message else ''
|
|
if any(k in message for k in ['dissatisfaction', 'dissatisfied', 'unhappy', 'not satisfied', 'complain']):
|
|
patient_dissatisfaction += 1
|
|
if any(k in message for k in ['no response', 'lack of response', 'no reply', 'delay']):
|
|
lack_of_response += 1
|
|
|
|
# Source breakdown
|
|
source_counts = {}
|
|
for c in complaints:
|
|
source = 'Unknown'
|
|
if c.source:
|
|
source_name = c.source.name_en.lower() if c.source.name_en else ''
|
|
if 'moh' in source_name:
|
|
source = 'MOH'
|
|
elif 'chi' in source_name:
|
|
source = 'CHI'
|
|
else:
|
|
source = c.source.name_en
|
|
elif c.complaint_source_type == 'external':
|
|
source = 'Patient/Relative'
|
|
else:
|
|
source = 'Internal'
|
|
|
|
source_counts[source] = source_counts.get(source, 0) + 1
|
|
|
|
# Location breakdown
|
|
location_counts = {}
|
|
for c in complaints:
|
|
loc = c.location.name_en if c.location else 'Unknown'
|
|
location_counts[loc] = location_counts.get(loc, 0) + 1
|
|
|
|
# Main department breakdown
|
|
main_dept_counts = {
|
|
'Medical': dept_counts.get('Medical', {}).get('total', 0),
|
|
'Nursing': dept_counts.get('Nursing', {}).get('total', 0),
|
|
'Admin': dept_counts.get('Admin', {}).get('total', 0),
|
|
'Support Services': dept_counts.get('Support Services', {}).get('total', 0)
|
|
}
|
|
|
|
# Top departments by complaints (detailed)
|
|
detailed_dept_counts = {}
|
|
for c in complaints:
|
|
if c.department:
|
|
dept_name = c.department.name
|
|
if dept_name not in detailed_dept_counts:
|
|
detailed_dept_counts[dept_name] = 0
|
|
detailed_dept_counts[dept_name] += 1
|
|
|
|
top_departments = sorted(detailed_dept_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
|
|
target_percentage = float(report.target_percentage)
|
|
|
|
prompt = f"""Analyze this N-PAD-001 Resolution to Patient Complaints KPI report.
|
|
|
|
REPORT DATA:
|
|
- Report Period: {report.year}-{report.month:02d}
|
|
- Hospital: {report.hospital.name}
|
|
- Resolution Rate: {overall_percentage:.2f}%
|
|
- Target: {target_percentage:.2f}%
|
|
- Total Complaints: {total_complaints}
|
|
- Resolved: {resolved_complaints}
|
|
- Closed: {closed_count}
|
|
|
|
CLOSED COMPLAINTS BY DEPARTMENT CATEGORY:
|
|
- Medical: {dept_counts.get('Medical', {}).get('closed', 0)}
|
|
- Nursing: {dept_counts.get('Nursing', {}).get('closed', 0)}
|
|
- Admin: {dept_counts.get('Admin', {}).get('closed', 0)}
|
|
- Support Services: {dept_counts.get('Support Services', {}).get('closed', 0)}
|
|
|
|
ESCALATED COMPLAINTS: {escalated_count}
|
|
- Due to Patient Dissatisfaction: {patient_dissatisfaction}
|
|
- Due to Lack of Response: {lack_of_response}
|
|
|
|
BY DEPARTMENT CATEGORY:
|
|
- Medical: {dept_counts.get('Medical', {}).get('escalated', 0)}
|
|
- Nursing: {dept_counts.get('Nursing', {}).get('escalated', 0)}
|
|
- Admin: {dept_counts.get('Admin', {}).get('escalated', 0)}
|
|
- Support: {dept_counts.get('Support Services', {}).get('escalated', 0)}
|
|
|
|
TOP DEPARTMENTS BY COMPLAINTS:
|
|
{chr(10).join([f"- {name}: {count}" for name, count in top_departments])}
|
|
|
|
SOURCE BREAKDOWN:
|
|
{chr(10).join([f"- {source}: {count}" for source, count in source_counts.items()])}
|
|
|
|
LOCATION BREAKDOWN:
|
|
{chr(10).join([f"- {loc}: {count}" for loc, count in location_counts.items()])}
|
|
|
|
MAIN DEPARTMENT BREAKDOWN:
|
|
{chr(10).join([f"- {dept}: {count}" for dept, count in main_dept_counts.items()])}
|
|
|
|
Provide analysis in JSON format:
|
|
{{
|
|
"executive_summary": "Overview of achieving {overall_percentage:.0f}% resolution rate",
|
|
"comparison_to_target": "Analysis of meeting {target_percentage:.0f}% target",
|
|
"closed_complaints_analysis": "Details about {closed_count} closed complaints",
|
|
"escalated_complaints_analysis": "Analysis of {escalated_count} escalated complaints",
|
|
"top_departments": [
|
|
"Department 1: X complaints",
|
|
"Department 2: Y complaints"
|
|
],
|
|
"department_breakdown": {{
|
|
"medical": "Medical dept analysis",
|
|
"nursing": "Nursing dept analysis",
|
|
"admin": "Admin dept analysis",
|
|
"support": "Support services analysis"
|
|
}},
|
|
"source_breakdown": {{
|
|
"moh": "MOH complaints analysis",
|
|
"chi": "CHI complaints analysis",
|
|
"patients": "Patient complaints analysis",
|
|
"relatives": "Relative complaints analysis"
|
|
}},
|
|
"location_breakdown": {{
|
|
"inpatient": "In-patient analysis",
|
|
"outpatient": "Out-patient analysis",
|
|
"er": "ER analysis"
|
|
}},
|
|
"recommendations": [
|
|
"Recommendation 1",
|
|
"Recommendation 2"
|
|
]
|
|
}}"""
|
|
|
|
system_prompt = """You are a healthcare KPI analysis expert for N-PAD-001 Resolution to Patient Complaints.
|
|
Analyze the complaint resolution data and provide:
|
|
1. Executive summary of resolution performance
|
|
2. Comparison to target achievement
|
|
3. Closed complaints breakdown by department
|
|
4. Escalated complaints analysis with reasons
|
|
5. Top departments with highest complaint counts
|
|
6. Source breakdown insights (MOH, CHI, Patients, Relatives)
|
|
7. Location breakdown insights (In-Patient, Out-Patient, ER)
|
|
8. Main department category analysis
|
|
9. Practical recommendations for maintaining/improving resolution rates
|
|
|
|
Be specific with numbers and focus on actionable insights."""
|
|
|
|
response = AIService.chat_completion(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
response_format="json_object",
|
|
temperature=0.3,
|
|
max_tokens=2500
|
|
)
|
|
|
|
import json
|
|
analysis = json.loads(response)
|
|
analysis['_metadata'] = {
|
|
'generated_at': timezone.now().isoformat(),
|
|
'report_id': str(report.id),
|
|
'report_type': report.report_type,
|
|
'hospital': report.hospital.name,
|
|
'year': report.year,
|
|
'month': report.month
|
|
}
|
|
|
|
report.ai_analysis = analysis
|
|
report.ai_analysis_generated_at = timezone.now()
|
|
report.save(update_fields=['ai_analysis', 'ai_analysis_generated_at'])
|
|
|
|
logger.info(f"AI analysis generated for N-PAD-001 report {report.id}")
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating N-PAD-001 analysis: {e}")
|
|
return {'error': str(e), 'executive_summary': 'Analysis failed'}
|
|
|
|
|
|
@classmethod
|
|
def _generate_satisfaction_resolution_analysis(cls, report: KPIReport) -> dict:
|
|
"""Generate AI analysis for Overall Satisfaction with Resolution (MOH-3) report"""
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
# Get report data
|
|
overall_percentage = float(report.overall_result) if report.overall_result else 0
|
|
total_responses = report.total_denominator
|
|
satisfied_responses = report.total_numerator
|
|
|
|
# Get monthly data
|
|
monthly_data = report.monthly_data.all().order_by('month')
|
|
monthly_breakdown = []
|
|
for md in monthly_data:
|
|
if md.denominator > 0:
|
|
monthly_breakdown.append({
|
|
'month': md.month,
|
|
'month_name': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][md.month - 1],
|
|
'percentage': float(md.percentage) if md.percentage else 0,
|
|
'satisfied': md.numerator,
|
|
'total': md.denominator
|
|
})
|
|
|
|
# Calculate response statistics from surveys
|
|
year_start = datetime(report.year, 1, 1)
|
|
year_end = datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
|
|
|
|
from apps.surveys.models import SurveyInstance, SurveyStatus
|
|
|
|
# Get resolution surveys for complaints in this period
|
|
surveys = SurveyInstance.objects.filter(
|
|
complaint__hospital=report.hospital,
|
|
complaint__created_at__gte=year_start,
|
|
complaint__created_at__lt=year_end,
|
|
template__survey_type='resolution',
|
|
status__in=[SurveyStatus.COMPLETED, SurveyStatus.PARTIAL]
|
|
).select_related('complaint', 'complaint__source')
|
|
|
|
total_surveys = surveys.count()
|
|
no_response = surveys.filter(status=SurveyStatus.PARTIAL).count()
|
|
completed_responses = surveys.filter(status=SurveyStatus.COMPLETED).count()
|
|
|
|
# Satisfaction breakdown
|
|
satisfied_count = 0
|
|
dissatisfied_count = 0
|
|
neutral_count = 0
|
|
|
|
for survey in surveys:
|
|
responses = survey.responses.all()
|
|
# Look for satisfaction rating (usually 1-5 or 1-10)
|
|
for resp in responses:
|
|
if resp.question and ('satisfaction' in resp.question.text.lower() or
|
|
'satisfied' in resp.question.text.lower()):
|
|
try:
|
|
value = int(resp.value)
|
|
if value >= 4: # 4-5 satisfied
|
|
satisfied_count += 1
|
|
elif value <= 2: # 1-2 dissatisfied
|
|
dissatisfied_count += 1
|
|
else: # 3 neutral
|
|
neutral_count += 1
|
|
except (ValueError, TypeError):
|
|
# Text response
|
|
value = str(resp.value).lower()
|
|
if any(w in value for w in ['satisfied', 'happy', 'good', 'excellent']):
|
|
satisfied_count += 1
|
|
elif any(w in value for w in ['dissatisfied', 'unhappy', 'bad', 'poor']):
|
|
dissatisfied_count += 1
|
|
else:
|
|
neutral_count += 1
|
|
|
|
# Calculate participation rate
|
|
participation_rate = (completed_responses / total_surveys * 100) if total_surveys > 0 else 0
|
|
|
|
# MOH specific satisfaction (from source)
|
|
moh_surveys = surveys.filter(complaint__source__name_en__icontains='MOH')
|
|
moh_satisfied = 0
|
|
moh_total = moh_surveys.count()
|
|
for survey in moh_surveys:
|
|
for resp in survey.responses.all():
|
|
if resp.question and ('satisfaction' in resp.question.text.lower()):
|
|
try:
|
|
if int(resp.value) >= 4:
|
|
moh_satisfied += 1
|
|
except (ValueError, TypeError):
|
|
pass
|
|
moh_satisfaction_rate = (moh_satisfied / moh_total * 100) if moh_total > 0 else 0
|
|
|
|
# Identify lowest month for analysis
|
|
lowest_month = min(monthly_breakdown, key=lambda x: x['percentage']) if monthly_breakdown else None
|
|
|
|
target_percentage = float(report.target_percentage)
|
|
|
|
prompt = f"""Analyze this Overall Satisfaction with Complaint Resolution (MOH-3) KPI report.
|
|
|
|
REPORT DATA:
|
|
- Report Period: {report.year}-{report.month:02d}
|
|
- Hospital: {report.hospital.name}
|
|
- Overall Satisfaction Rate: {overall_percentage:.2f}%
|
|
- Target: {target_percentage:.2f}%
|
|
- Total Complaints Received: {total_responses}
|
|
|
|
MONTHLY SATISFACTION TRENDS:
|
|
{chr(10).join([f"- {m['month_name']}: {m['percentage']:.2f}%" for m in monthly_breakdown])}
|
|
|
|
RESPONSE STATISTICS:
|
|
- Total Surveys Sent: {total_surveys}
|
|
- Completed Responses: {completed_responses}
|
|
- No Response: {no_response}
|
|
- Participation Rate: {participation_rate:.2f}%
|
|
|
|
SATISFACTION BREAKDOWN:
|
|
- Satisfied: {satisfied_count}
|
|
- Dissatisfied: {dissatisfied_count}
|
|
- Neutral: {neutral_count}
|
|
|
|
MOH SPECIFIC:
|
|
- MOH Satisfaction Rate: {moh_satisfaction_rate:.2f}%
|
|
- Total MOH Responses: {moh_total}
|
|
|
|
LOWEST PERFORMANCE MONTH:
|
|
{lowest_month['month_name'] if lowest_month else 'N/A'}: {lowest_month['percentage']:.2f}%{' (significant drop)' if lowest_month and lowest_month['percentage'] < 50 else ''}
|
|
|
|
SATISFACTION AFTER EXCLUDING NO RESPONSE:
|
|
{(satisfied_count / completed_responses * 100) if completed_responses > 0 else 0:.2f}%
|
|
|
|
Provide analysis in JSON format:
|
|
{{
|
|
"executive_summary": "Overview of satisfaction performance at {overall_percentage:.2f}%",
|
|
"satisfaction_rate_by_month": [
|
|
"Month: Percentage%"
|
|
],
|
|
"moh_satisfaction_analysis": "Analysis of MOH satisfaction at {moh_satisfaction_rate:.2f}%",
|
|
"performance_overview": "Satisfaction rate after excluding no-response is X%",
|
|
"participation_rate_analysis": "Response rate is {participation_rate:.2f}% - analysis of patient engagement",
|
|
"key_issues": [
|
|
"Issue 1: Long resolution times causing frustration",
|
|
"Issue 2: Patients dissatisfied with outcomes",
|
|
"Issue 3: etc."
|
|
],
|
|
"response_statistics": {{
|
|
"total_complaints": {total_responses},
|
|
"total_responses": {completed_responses},
|
|
"no_response": {no_response},
|
|
"escalated_without_response": 0,
|
|
"closed_without_response": 0,
|
|
"satisfied": {satisfied_count},
|
|
"dissatisfied": {dissatisfied_count},
|
|
"neutral": {neutral_count}
|
|
}},
|
|
"recommendations": [
|
|
"Recommendation 1",
|
|
"Recommendation 2"
|
|
]
|
|
}}"""
|
|
|
|
system_prompt = """You are a healthcare patient satisfaction analysis expert for MOH-3 Overall Satisfaction with Complaint Resolution.
|
|
Analyze the satisfaction survey data and provide:
|
|
1. Executive summary of overall satisfaction performance
|
|
2. Month-by-month satisfaction rate analysis with trends
|
|
3. MOH-specific satisfaction analysis
|
|
4. Performance overview after excluding non-responses
|
|
5. Participation rate analysis and patient engagement insights
|
|
6. Key issues causing dissatisfaction (long wait times, poor outcomes, etc.)
|
|
7. Response statistics breakdown
|
|
8. Actionable recommendations for improving satisfaction
|
|
|
|
Focus on identifying why patients are dissatisfied and provide practical solutions."""
|
|
|
|
response = AIService.chat_completion(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
response_format="json_object",
|
|
temperature=0.3,
|
|
max_tokens=2500
|
|
)
|
|
|
|
import json
|
|
analysis = json.loads(response)
|
|
analysis['_metadata'] = {
|
|
'generated_at': timezone.now().isoformat(),
|
|
'report_id': str(report.id),
|
|
'report_type': report.report_type,
|
|
'hospital': report.hospital.name,
|
|
'year': report.year,
|
|
'month': report.month
|
|
}
|
|
|
|
report.ai_analysis = analysis
|
|
report.ai_analysis_generated_at = timezone.now()
|
|
report.save(update_fields=['ai_analysis', 'ai_analysis_generated_at'])
|
|
|
|
logger.info(f"AI analysis generated for Satisfaction Resolution report {report.id}")
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating Satisfaction Resolution analysis: {e}")
|
|
return {'error': str(e), 'executive_summary': 'Analysis failed'}
|
|
|
|
|
|
@classmethod
|
|
def _generate_response_rate_analysis(cls, report: KPIReport) -> dict:
|
|
"""Generate AI analysis for Department Response Rate (Dep-KPI-4) report"""
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
# Get report data
|
|
overall_percentage = float(report.overall_result) if report.overall_result else 0
|
|
total_complaints = report.total_denominator
|
|
responded_within_48h = report.total_numerator
|
|
|
|
# Get date range
|
|
year_start = datetime(report.year, 1, 1)
|
|
year_end = datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
|
|
|
|
# Query complaints needing department response
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=year_start,
|
|
created_at__lt=year_end,
|
|
complaint_type="complaint",
|
|
status__in=[ComplaintStatus.OPEN, ComplaintStatus.IN_PROGRESS, ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED]
|
|
).select_related('department', 'location', 'main_section').prefetch_related('updates')
|
|
|
|
# Calculate response times
|
|
responded_48_72h = 0
|
|
responded_over_72h = 0
|
|
dept_response_times = {}
|
|
|
|
for c in complaints:
|
|
# Find first department response
|
|
first_response = c.updates.filter(
|
|
update_type='response',
|
|
created_by__isnull=False
|
|
).order_by('created_at').first()
|
|
|
|
if first_response and c.created_at:
|
|
hours = (first_response.created_at - c.created_at).total_seconds() / 3600
|
|
dept_name = c.department.name if c.department else "Unassigned"
|
|
|
|
if dept_name not in dept_response_times:
|
|
dept_response_times[dept_name] = []
|
|
dept_response_times[dept_name].append(hours / 24)
|
|
|
|
if 48 < hours <= 72:
|
|
responded_48_72h += 1
|
|
elif hours > 72:
|
|
responded_over_72h += 1
|
|
|
|
# Calculate average response times for slow departments (>48 hours)
|
|
slow_departments = []
|
|
for dept, days_list in dept_response_times.items():
|
|
if days_list:
|
|
avg_days = sum(days_list) / len(days_list)
|
|
if avg_days > 2: # More than 48 hours
|
|
slow_departments.append({
|
|
'name': dept,
|
|
'avg_days': round(avg_days, 1),
|
|
'complaint_count': len(days_list)
|
|
})
|
|
slow_departments.sort(key=lambda x: x['avg_days'], reverse=True)
|
|
|
|
# Categorize slow departments
|
|
medical_slow = []
|
|
nursing_slow = []
|
|
admin_slow = []
|
|
support_slow = []
|
|
|
|
for dept in slow_departments:
|
|
name_lower = dept['name'].lower()
|
|
if any(k in name_lower for k in ['medical', 'surgery', 'cardiology', 'orthopedics', 'pediatrics', 'obstetrics', 'gynecology', 'er', 'lab', 'icu', 'clinic', 'ward', 'physiotherapy', 'psychiatric']):
|
|
medical_slow.append(dept)
|
|
elif any(k in name_lower for k in ['nursing', 'nurse', 'opd nursing', 'icu nursing']):
|
|
nursing_slow.append(dept)
|
|
elif any(k in name_lower for k in ['admin', 'reception', 'manager', 'approval', 'report']):
|
|
admin_slow.append(dept)
|
|
elif any(k in name_lower for k in ['housekeeping', 'maintenance', 'security', 'cafeteria', 'transport', 'support']):
|
|
support_slow.append(dept)
|
|
|
|
# Count total exceeding 48 hours
|
|
total_exceeding_48h = responded_48_72h + responded_over_72h
|
|
|
|
target_percentage = float(report.target_percentage)
|
|
threshold_percentage = 70.0 # Standard threshold
|
|
|
|
prompt = f"""Analyze this Department Response Rate (Dep-KPI-4) KPI report.
|
|
|
|
REPORT DATA:
|
|
- Report Period: {report.year}-{report.month:02d}
|
|
- Hospital: {report.hospital.name}
|
|
- Response Rate (≤48h): {overall_percentage:.2f}%
|
|
- Target: {target_percentage:.2f}%
|
|
- Threshold: {threshold_percentage:.2f}%
|
|
- Status: {"Target Met" if overall_percentage >= target_percentage else "Below Target" if overall_percentage >= threshold_percentage else "Below Threshold"}
|
|
- Total Complaints: {total_complaints}
|
|
- Responded within 48h: {responded_within_48h}
|
|
|
|
COMPLAINTS EXCEEDING 48 HOURS: {total_exceeding_48h}
|
|
- 48-72 hours: {responded_48_72h} complaints ({(responded_48_72h/total_complaints*100) if total_complaints else 0:.2f}%)
|
|
- Over 72 hours: {responded_over_72h} complaints ({(responded_over_72h/total_complaints*100) if total_complaints else 0:.2f}%)
|
|
|
|
SLOW DEPARTMENTS (Avg > 48 hours):
|
|
MEDICAL DEPARTMENT:
|
|
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days" for d in medical_slow[:10]]) if medical_slow else "- None exceeding timeframe"}
|
|
|
|
NURSING DEPARTMENT:
|
|
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days" for d in nursing_slow[:5]]) if nursing_slow else "- None exceeding timeframe"}
|
|
|
|
NON-MEDICAL DEPARTMENT:
|
|
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days" for d in admin_slow[:5]]) if admin_slow else "- All within timeframe"}
|
|
|
|
SUPPORT SERVICES:
|
|
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days" for d in support_slow[:5]]) if support_slow else "- No complaints received"}
|
|
|
|
Provide analysis in JSON format:
|
|
{{
|
|
"executive_summary": "Overview of {overall_percentage:.2f}% response rate vs {target_percentage:.0f}% target",
|
|
"comparison_to_target": "Analysis of meeting target and threshold",
|
|
"response_rate": "{overall_percentage:.2f}%",
|
|
"complaints_exceeding_48h": {{
|
|
"total": {total_exceeding_48h},
|
|
"within_48_72h": {{"count": {responded_48_72h}, "percentage": "{(responded_48_72h/total_complaints*100) if total_complaints else 0:.2f}%"}},
|
|
"over_72h": {{"count": {responded_over_72h}, "percentage": "{(responded_over_72h/total_complaints*100) if total_complaints else 0:.2f}%"}}
|
|
}},
|
|
"slow_departments": {{
|
|
"medical": [
|
|
"Dept Name: X Days"
|
|
],
|
|
"nursing": [
|
|
"Dept Name: X Days"
|
|
],
|
|
"non_medical": "All within timeframe OR list",
|
|
"support_services": "No complaints OR list"
|
|
}},
|
|
"recommendations": [
|
|
"Recommendation 1: Follow up with department heads",
|
|
"Recommendation 2: Contact relevant persons directly"
|
|
]
|
|
}}"""
|
|
|
|
system_prompt = """You are a healthcare department response analysis expert for Dep-KPI-4 Department Response Rate.
|
|
Analyze the response time data and provide:
|
|
1. Executive summary of response rate performance
|
|
2. Comparison to target and threshold (70%)
|
|
3. Breakdown of complaints exceeding 48 hours
|
|
4. Analysis of slow departments by category (Medical, Nursing, Non-Medical, Support)
|
|
5. Specific department names with their average response times
|
|
6. Actionable recommendations for improving response times
|
|
|
|
Focus on identifying which departments need improvement and practical follow-up strategies."""
|
|
|
|
response = AIService.chat_completion(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
response_format="json_object",
|
|
temperature=0.3,
|
|
max_tokens=2000
|
|
)
|
|
|
|
import json
|
|
analysis = json.loads(response)
|
|
analysis['_metadata'] = {
|
|
'generated_at': timezone.now().isoformat(),
|
|
'report_id': str(report.id),
|
|
'report_type': report.report_type,
|
|
'hospital': report.hospital.name,
|
|
'year': report.year,
|
|
'month': report.month
|
|
}
|
|
|
|
report.ai_analysis = analysis
|
|
report.ai_analysis_generated_at = timezone.now()
|
|
report.save(update_fields=['ai_analysis', 'ai_analysis_generated_at'])
|
|
|
|
logger.info(f"AI analysis generated for Response Rate report {report.id}")
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating Response Rate analysis: {e}")
|
|
return {'error': str(e), 'executive_summary': 'Analysis failed'}
|
|
|
|
|
|
@classmethod
|
|
def _generate_activation_2h_analysis(cls, report: KPIReport) -> dict:
|
|
"""Generate AI analysis for Complaint Activation Within 2 Hours (KPI-6) report"""
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
# Get report data
|
|
overall_percentage = float(report.overall_result) if report.overall_result else 0
|
|
total_complaints = report.total_denominator
|
|
activated_within_2h = report.total_numerator
|
|
|
|
# Get date range
|
|
year_start = datetime(report.year, 1, 1)
|
|
year_end = datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
|
|
|
|
# Query complaints for activation analysis
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=year_start,
|
|
created_at__lt=year_end,
|
|
complaint_type="complaint"
|
|
).select_related('assigned_to', 'activated_by').prefetch_related('updates')
|
|
|
|
# Calculate activation statistics
|
|
activated_complaints = []
|
|
delayed_complaints = []
|
|
|
|
# Staff activation breakdown
|
|
staff_activation = {}
|
|
staff_delays = {}
|
|
|
|
for c in complaints:
|
|
# Check if complaint was activated
|
|
activation_update = c.updates.filter(
|
|
update_type='status_change',
|
|
message__icontains='activat'
|
|
).order_by('created_at').first()
|
|
|
|
if activation_update and c.created_at:
|
|
hours = (activation_update.created_at - c.created_at).total_seconds() / 3600
|
|
|
|
# Get staff who activated
|
|
staff_name = activation_update.created_by.get_full_name() if activation_update.created_by else "Unknown"
|
|
|
|
if hours <= 2:
|
|
activated_complaints.append({
|
|
'id': str(c.id),
|
|
'hours': hours,
|
|
'staff': staff_name
|
|
})
|
|
|
|
if staff_name not in staff_activation:
|
|
staff_activation[staff_name] = 0
|
|
staff_activation[staff_name] += 1
|
|
else:
|
|
delayed_complaints.append({
|
|
'id': str(c.id),
|
|
'hours': hours,
|
|
'staff': staff_name
|
|
})
|
|
|
|
if staff_name not in staff_delays:
|
|
staff_delays[staff_name] = 0
|
|
staff_delays[staff_name] += 1
|
|
|
|
# Sort staff by activation count
|
|
top_activators = sorted(staff_activation.items(), key=lambda x: x[1], reverse=True)
|
|
top_delays = sorted(staff_delays.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
# Calculate percentages
|
|
total_count = len(activated_complaints) + len(delayed_complaints)
|
|
activated_count = len(activated_complaints)
|
|
delay_count = len(delayed_complaints)
|
|
|
|
activated_percentage = (activated_count / total_count * 100) if total_count > 0 else 0
|
|
delay_percentage = (delay_count / total_count * 100) if total_count > 0 else 0
|
|
|
|
target_percentage = float(report.target_percentage)
|
|
threshold_percentage = 80.0 # Typical threshold for activation
|
|
|
|
# Compare to previous month if available
|
|
prev_month = report.month - 1 if report.month > 1 else 12
|
|
prev_year = report.year if report.month > 1 else report.year - 1
|
|
|
|
try:
|
|
prev_report = KPIReport.objects.get(
|
|
report_type=report.report_type,
|
|
hospital=report.hospital,
|
|
year=prev_year,
|
|
month=prev_month
|
|
)
|
|
prev_percentage = float(prev_report.overall_result) if prev_report.overall_result else 0
|
|
except KPIReport.DoesNotExist:
|
|
prev_percentage = None
|
|
|
|
prompt = f"""Analyze this Complaint Activation Within 2 Hours (KPI-6) report.
|
|
|
|
REPORT DATA:
|
|
- Report Period: {report.year}-{report.month:02d}
|
|
- Hospital: {report.hospital.name}
|
|
- Activation Rate (≤2h): {overall_percentage:.2f}%
|
|
- Target: {target_percentage:.2f}%
|
|
- Threshold: {threshold_percentage:.2f}%
|
|
- Status: {"Target Met" if overall_percentage >= target_percentage else "Below Target" if overall_percentage >= threshold_percentage else "Below Threshold"}
|
|
- Total Complaints: {total_count}
|
|
|
|
PREVIOUS MONTH COMPARISON:
|
|
{prev_month}/{prev_year}: {prev_percentage:.2f}% {'(improved)' if prev_percentage and overall_percentage > prev_percentage else '(declined)' if prev_percentage and overall_percentage < prev_percentage else '(stable)' if prev_percentage else '(no data)'}
|
|
|
|
ACTIVATED WITHIN 2 HOURS:
|
|
- Total: {activated_count} Complaints ({activated_percentage:.2f}%)
|
|
|
|
BY STAFF:
|
|
{chr(10).join([f"- {name}: {count} Complaints - {(count/activated_count*100) if activated_count else 0:.2f}%" for name, count in top_activators[:5]]) if top_activators else "- No data"}
|
|
|
|
DELAYS IN ACTIVATION:
|
|
- Total: {delay_count} Complaints ({delay_percentage:.2f}%)
|
|
|
|
BY STAFF:
|
|
{chr(10).join([f"- {name}: {count} Complaints - {(count/delay_count*100) if delay_count else 0:.2f}%" for name, count in top_delays[:5]]) if top_delays else "- No delays"}
|
|
|
|
Provide analysis in JSON format:
|
|
{{
|
|
"executive_summary": "Overview of {overall_percentage:.2f}% activation rate",
|
|
"comparison_to_target": "Analysis vs {target_percentage:.0f}% target and {threshold_percentage:.0f}% threshold",
|
|
"trend_analysis": "Comparison to previous month: improved/declined/stable",
|
|
"delay_reasons": [
|
|
"Reason 1: Staff attempts to resolve immediately but patient refuses",
|
|
"Reason 2: Unclear complaint content requiring clarification",
|
|
"Reason 3: Ensuring correct department direction"
|
|
],
|
|
"activation_statistics": {{
|
|
"activated_within_2h": {{
|
|
"count": {activated_count},
|
|
"percentage": "{activated_percentage:.2f}%",
|
|
"by_staff": [
|
|
"Staff Name: X Complaints - Y%"
|
|
]
|
|
}},
|
|
"delays": {{
|
|
"count": {delay_count},
|
|
"percentage": "{delay_percentage:.2f}%",
|
|
"by_staff": [
|
|
"Staff Name: X Complaints - Y%"
|
|
]
|
|
}}
|
|
}},
|
|
"recommendations": [
|
|
"Establish timer notification in complaints platform",
|
|
"Add delay reason feature to complaints platform"
|
|
]
|
|
}}"""
|
|
|
|
system_prompt = """You are a healthcare complaint activation analysis expert for KPI-6 Activation Within 2 Hours.
|
|
Analyze the activation time data and provide:
|
|
1. Executive summary of activation rate performance
|
|
2. Comparison to target and threshold
|
|
3. Trend analysis comparing to previous month
|
|
4. Reasons for activation delays
|
|
5. Activation statistics by staff member
|
|
6. Delay statistics by staff member
|
|
7. Actionable recommendations for improving activation times
|
|
|
|
Focus on identifying why activations are delayed and practical solutions."""
|
|
|
|
response = AIService.chat_completion(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
response_format="json_object",
|
|
temperature=0.3,
|
|
max_tokens=2000
|
|
)
|
|
|
|
import json
|
|
analysis = json.loads(response)
|
|
analysis['_metadata'] = {
|
|
'generated_at': timezone.now().isoformat(),
|
|
'report_id': str(report.id),
|
|
'report_type': report.report_type,
|
|
'hospital': report.hospital.name,
|
|
'year': report.year,
|
|
'month': report.month
|
|
}
|
|
|
|
report.ai_analysis = analysis
|
|
report.ai_analysis_generated_at = timezone.now()
|
|
report.save(update_fields=['ai_analysis', 'ai_analysis_generated_at'])
|
|
|
|
logger.info(f"AI analysis generated for Activation 2h report {report.id}")
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating Activation 2h analysis: {e}")
|
|
return {'error': str(e), 'executive_summary': 'Analysis failed'}
|
|
|
|
|
|
@classmethod
|
|
def _generate_unactivated_analysis(cls, report: KPIReport) -> dict:
|
|
"""Generate AI analysis for Unactivated Filled Complaints Rate (KPI-7) report"""
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
# Get report data
|
|
overall_percentage = float(report.overall_result) if report.overall_result else 0
|
|
total_filled_complaints = report.total_denominator
|
|
unactivated_complaints = report.total_numerator
|
|
|
|
# Get date range
|
|
year_start = datetime(report.year, 1, 1)
|
|
year_end = datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
|
|
|
|
# Query complaints for unactivated analysis
|
|
complaints = Complaint.objects.filter(
|
|
hospital=report.hospital,
|
|
created_at__gte=year_start,
|
|
created_at__lt=year_end,
|
|
complaint_type="complaint"
|
|
).select_related('created_by', 'source').prefetch_related('updates')
|
|
|
|
# Categorize complaints
|
|
on_hold_count = 0
|
|
not_filled_count = 0
|
|
filled_count = 0
|
|
barcode_count = 0
|
|
|
|
# Unactivation reasons
|
|
reasons = {
|
|
'incomplete_by_patient': 0,
|
|
'resolved_immediately': 0,
|
|
'not_meet_criteria': 0,
|
|
'transferred_to_observation': 0,
|
|
'complainant_withdraw': 0,
|
|
'repeated_from_chi': 0,
|
|
'other': 0
|
|
}
|
|
|
|
# Staff breakdown
|
|
staff_stats = {}
|
|
|
|
# Time to fill breakdown
|
|
filled_same_time = 0
|
|
filled_within_6h = 0
|
|
filled_6h_to_24h = 0
|
|
filled_after_1d = 0
|
|
time_not_mentioned = 0
|
|
|
|
for c in complaints:
|
|
# Check status and categorization
|
|
if c.status == 'open' or c.status == 'pending':
|
|
on_hold_count += 1
|
|
|
|
# Check if filled or not
|
|
updates = c.updates.all()
|
|
has_content = len(c.description) > 50 if c.description else False
|
|
|
|
if has_content:
|
|
filled_count += 1
|
|
else:
|
|
not_filled_count += 1
|
|
|
|
# Track staff
|
|
staff_name = c.created_by.get_full_name() if c.created_by else "Unknown"
|
|
if staff_name not in staff_stats:
|
|
staff_stats[staff_name] = {'filled': 0, 'not_filled': 0, 'total': 0}
|
|
staff_stats[staff_name]['total'] += 1
|
|
|
|
if has_content:
|
|
staff_stats[staff_name]['filled'] += 1
|
|
else:
|
|
staff_stats[staff_name]['not_filled'] += 1
|
|
|
|
# Analyze unactivation reasons from updates/metadata
|
|
metadata = c.metadata or {}
|
|
reason = metadata.get('unactivation_reason', '').lower()
|
|
|
|
if 'incomplete' in reason or 'patient' in reason:
|
|
reasons['incomplete_by_patient'] += 1
|
|
elif 'resolved' in reason or 'immediate' in reason:
|
|
reasons['resolved_immediately'] += 1
|
|
elif 'criteria' in reason or 'not meet' in reason:
|
|
reasons['not_meet_criteria'] += 1
|
|
elif 'observation' in reason or 'transferred' in reason:
|
|
reasons['transferred_to_observation'] += 1
|
|
elif 'withdraw' in reason:
|
|
reasons['complainant_withdraw'] += 1
|
|
elif 'chi' in reason or 'repeated' in reason:
|
|
reasons['repeated_from_chi'] += 1
|
|
else:
|
|
reasons['other'] += 1
|
|
|
|
# Build staff breakdown table
|
|
staff_table = []
|
|
for staff, stats in staff_stats.items():
|
|
staff_table.append({
|
|
'name': staff,
|
|
'total': stats['total'],
|
|
'filled': stats['filled'],
|
|
'not_filled': stats['not_filled']
|
|
})
|
|
|
|
# Sort by total
|
|
staff_table.sort(key=lambda x: x['total'], reverse=True)
|
|
|
|
target_percentage = float(report.target_percentage)
|
|
threshold_percentage = 5.0 # Standard threshold
|
|
|
|
prompt = f"""Analyze this Unactivated Filled Complaints Rate (KPI-7) report.
|
|
|
|
REPORT DATA:
|
|
- Report Period: {report.year}-{report.month:02d}
|
|
- Hospital: {report.hospital.name}
|
|
- Unactivated Rate: {overall_percentage:.2f}%
|
|
- Target: {target_percentage:.2f}%
|
|
- Threshold: {threshold_percentage:.2f}%
|
|
- Status: {"Above Threshold (Action Needed)" if overall_percentage > threshold_percentage else "Within Acceptable Range"}
|
|
|
|
COMPLAINT BREAKDOWN:
|
|
- On Hold: {on_hold_count} Complaints
|
|
- Not Filled: {not_filled_count} Complaints
|
|
- Filled: {filled_count} Complaints
|
|
- From Barcode: {barcode_count} Complaints
|
|
|
|
TIME TO FILL ANALYSIS:
|
|
- Same Time: {filled_same_time} Complaints
|
|
- Within 6 Hours: {filled_within_6h} Complaints
|
|
- 6 Hours to 24 Hours: {filled_6h_to_24h} Complaints
|
|
- After 1 Day: {filled_after_1d} Complaints
|
|
- Time Not Mentioned: {time_not_mentioned} Complaints
|
|
|
|
UNACTIVATION REASONS:
|
|
- Incomplete by Patient: {reasons['incomplete_by_patient']} Requests
|
|
- Resolved Immediately: {reasons['resolved_immediately']} Requests
|
|
- Do Not Meet Criteria: {reasons['not_meet_criteria']} Requests
|
|
- Transferred to Observations: {reasons['transferred_to_observation']} Requests
|
|
- Complainant Withdraw: {reasons['complainant_withdraw']} Request
|
|
- Repeated from CHI: {reasons['repeated_from_chi']} Request
|
|
|
|
STAFF BREAKDOWN:
|
|
{chr(10).join([f"- {s['name']}: Total {s['total']}, Filled {s['filled']}, Not Filled {s['not_filled']}" for s in staff_table[:10]])}
|
|
|
|
Provide analysis in JSON format:
|
|
{{
|
|
"executive_summary": "Overview of {overall_percentage:.2f}% unactivated rate vs {threshold_percentage:.0f}% threshold",
|
|
"threshold_analysis": "Rate exceeds acceptable threshold of {threshold_percentage:.0f}% - detailed analysis",
|
|
"unactivation_reasons": [
|
|
"X Requests - left incomplete by the patient",
|
|
"X Requests - resolved immediately",
|
|
"X Requests - do not meet activation criteria",
|
|
"X Requests - transferred to observations",
|
|
"X Request - complainant withdraw",
|
|
"X Request - repeated from CHI"
|
|
],
|
|
"complaint_statistics": {{
|
|
"on_hold": {on_hold_count},
|
|
"not_filled": {not_filled_count},
|
|
"filled": {filled_count},
|
|
"from_barcode": {barcode_count}
|
|
}},
|
|
"time_to_fill": {{
|
|
"same_time": {filled_same_time},
|
|
"within_6h": {filled_within_6h},
|
|
"6h_to_24h": {filled_6h_to_24h},
|
|
"after_1d": {filled_after_1d},
|
|
"not_mentioned": {time_not_mentioned}
|
|
}},
|
|
"staff_breakdown": [
|
|
"Staff Name: Total X, Filled Y, Not Filled Z"
|
|
],
|
|
"recommendations": [
|
|
"Review Complaints Portal Daily by Patient Relations Supervisor",
|
|
"Check SMS messages for filled complaints immediately",
|
|
"Establish notification reminders for staff follow-up"
|
|
]
|
|
}}"""
|
|
|
|
system_prompt = """You are a healthcare complaint management analysis expert for KPI-7 Unactivated Filled Complaints Rate.
|
|
Analyze the unactivation data and provide:
|
|
1. Executive summary of unactivation rate vs threshold
|
|
2. Threshold analysis (5% is the acceptable limit)
|
|
3. Detailed unactivation reasons breakdown
|
|
4. Complaint statistics (on hold, filled, not filled)
|
|
5. Time to fill analysis
|
|
6. Staff breakdown and performance
|
|
7. Actionable recommendations for reducing unactivated complaints
|
|
|
|
Focus on why complaints remain unactivated and practical solutions."""
|
|
|
|
response = AIService.chat_completion(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
response_format="json_object",
|
|
temperature=0.3,
|
|
max_tokens=2000
|
|
)
|
|
|
|
import json
|
|
analysis = json.loads(response)
|
|
analysis['_metadata'] = {
|
|
'generated_at': timezone.now().isoformat(),
|
|
'report_id': str(report.id),
|
|
'report_type': report.report_type,
|
|
'hospital': report.hospital.name,
|
|
'year': report.year,
|
|
'month': report.month
|
|
}
|
|
|
|
report.ai_analysis = analysis
|
|
report.ai_analysis_generated_at = timezone.now()
|
|
report.save(update_fields=['ai_analysis', 'ai_analysis_generated_at'])
|
|
|
|
logger.info(f"AI analysis generated for Unactivated report {report.id}")
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error generating Unactivated analysis: {e}")
|
|
return {'error': str(e), 'executive_summary': 'Analysis failed'}
|