HH/apps/analytics/kpi_service.py
2026-02-25 04:47:05 +03:00

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'}