HH/apps/analytics/kpi_service.py
2026-03-28 14:03:56 +03:00

2366 lines
96 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,
KPIReportLocationBreakdown,
KPIReportMonthlyData,
KPIReportSourceBreakdown,
KPIReportStatus,
KPIReportType,
)
DEPARTMENT_CATEGORY_KEYWORDS = {
"medical": [
"medical",
"surgery",
"cardiology",
"orthopedics",
"pediatrics",
"obstetrics",
"gynecology",
"er",
"emergency",
"lab",
"icu",
"clinic",
"ward",
"physiotherapy",
"psychiatric",
"ophthalmology",
"pharmacy",
"radiology",
"pathology",
"anesthesia",
"ent",
"dermatology",
"urology",
"neurology",
"oncology",
"nursery",
],
"nursing": [
"nursing",
"nurse",
"iv medication room nursing",
"long term nursing",
"vaccination room nursing",
"pediatric inpatient",
],
"admin": [
"administration",
"admin",
"reception",
"manager",
"approval",
"report",
"finance",
"it",
"hr",
"medical reports",
"appointments",
"on-duty",
"opd reception",
"outpatient reception",
],
"support": [
"housekeeping",
"maintenance",
"security",
"cafeteria",
"transport",
"support",
],
}
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,
"target_percentage": (
0.00
if report_type == KPIReportType.UNACTIVATED
else 85.00
if report_type == KPIReportType.PATIENT_EXPERIENCE
else 80.00
if report_type == KPIReportType.RESPONSE_RATE
else 95.00
),
"threshold_percentage": (
5.00
if report_type == KPIReportType.UNACTIVATED
else 78.00
if report_type == KPIReportType.PATIENT_EXPERIENCE
else 70.00
if report_type == KPIReportType.RESPONSE_RATE
else 90.00
),
"threshold_percentage": (
78.00
if report_type == KPIReportType.PATIENT_EXPERIENCE
else 70.00
if report_type == KPIReportType.RESPONSE_RATE
else 90.00
),
},
)
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.PATIENT_EXPERIENCE,
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)
# Create location breakdown
cls._create_location_breakdowns(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": float(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()
all_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=(
datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
),
complaint_type="complaint",
)
cls._create_source_breakdowns(report, all_complaints)
cls._create_department_breakdown(report, all_complaints)
cls._create_location_breakdowns(report, all_complaints)
@classmethod
def _calculate_n_pad_001(cls, report: KPIReport):
"""Calculate N-PAD-001 Resolution Rate"""
year_start = datetime(report.year, 1, 1)
if report.month == 12:
year_end = datetime(report.year + 1, 1, 1)
else:
year_end = 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)
complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=month_start,
created_at__lt=month_end,
complaint_type="complaint",
)
denominator = complaints.count()
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()
all_complaints = Complaint.objects.filter(
hospital=report.hospital, created_at__gte=year_start, created_at__lt=year_end, complaint_type="complaint"
)
cls._create_source_breakdowns(report, all_complaints)
cls._create_department_breakdown(report, all_complaints)
cls._create_location_breakdowns(report, all_complaints)
@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)
complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=month_start,
created_at__lt=month_end,
complaint_type="complaint",
)
denominator = complaints.count()
numerator = 0
for complaint in complaints:
first_response = (
complaint.updates.filter(update_type__in=["communication", "resolution"])
.order_by("created_at")
.first()
)
if first_response and complaint.created_at:
response_time = first_response.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()
all_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=(
datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
),
complaint_type="complaint",
)
cls._create_source_breakdowns(report, all_complaints)
cls._create_department_breakdown(report, all_complaints)
cls._create_location_breakdowns(report, all_complaints)
@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()
all_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=(
datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
),
complaint_type="complaint",
)
cls._create_source_breakdowns(report, all_complaints)
cls._create_department_breakdown(report, all_complaints)
cls._create_location_breakdowns(report, all_complaints)
@classmethod
def _calculate_unactivated(cls, report: KPIReport):
"""Calculate Unactivated Filled Complaints Rate"""
from apps.dashboard.models import ComplaintRequest
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)
complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=month_start,
created_at__lt=month_end,
complaint_type="complaint",
)
denominator = complaints.count()
filled_unactivated = (
complaints.filter(
complaint_request_records__filled=True,
assigned_to__isnull=True,
)
.distinct()
.count()
)
monthly_data, _ = KPIReportMonthlyData.objects.get_or_create(
kpi_report=report,
month=month,
defaults={
"numerator": filled_unactivated,
"denominator": denominator,
},
)
monthly_data.numerator = filled_unactivated
monthly_data.denominator = denominator
monthly_data.calculate_percentage()
monthly_data.is_below_target = monthly_data.percentage > report.threshold_percentage
monthly_data.save()
total_numerator += filled_unactivated
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()
all_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=(
datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
),
complaint_type="complaint",
)
cls._create_source_breakdowns(report, all_complaints)
cls._create_department_breakdown(report, all_complaints)
cls._create_location_breakdowns(report, all_complaints)
@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"""
report.department_breakdowns.all().delete()
for category, keywords in DEPARTMENT_CATEGORY_KEYWORDS.items():
q_objects = Q()
for keyword in keywords:
q_objects |= Q(department__name__icontains=keyword)
dept_complaints = complaints.filter(q_objects).distinct()
complaint_count = dept_complaints.count()
resolved_count = dept_complaints.filter(
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED]
).count()
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()))
top_areas_list = []
seen = set()
for c in dept_complaints[:20]:
if c.category:
name = c.category.name_en
if name not in seen:
top_areas_list.append(name)
seen.add(name)
top_areas = "\n".join(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 _create_location_breakdowns(cls, report: KPIReport, complaints):
"""Create location breakdown records (In-Patient, Out-Patient, ER)"""
report.location_breakdowns.all().delete()
location_categories = {
"In-Patient": ["inpatient", "in-patient", "ip", "in patient"],
"Out-Patient": ["outpatient", "out-patient", "opd", "out patient", "op"],
"ER": ["emergency", "er", "ed", "a&e", "accident"],
}
total = complaints.count()
if total == 0:
return
for loc_type, keywords in location_categories.items():
q_objects = Q()
for keyword in keywords:
q_objects |= Q(location__name_en__icontains=keyword)
q_objects |= Q(main_section__name_en__icontains=keyword)
loc_complaints = complaints.filter(q_objects).distinct()
count = loc_complaints.count()
percentage = (count / total * 100) if total > 0 else 0
KPIReportLocationBreakdown.objects.create(
kpi_report=report,
location_type=loc_type,
complaint_count=count,
percentage=Decimal(str(round(percentage, 2))),
)
@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.PATIENT_EXPERIENCE:
return cls._generate_patient_experience_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:
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)
)
all_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=year_end,
complaint_type="complaint",
)
complaints = all_complaints.filter(
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED],
)
resolved_count = complaints.count()
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)
# G2: Categorize slow departments into 4 categories
medical_slow = []
nursing_slow = []
admin_slow = []
support_slow = []
for dept, days_list in dept_response_times.items():
if days_list:
avg_days = sum(days_list) / len(days_list)
entry = {"name": dept, "avg_days": round(avg_days, 1), "count": len(days_list)}
name_lower = dept.lower()
if any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["medical"]):
medical_slow.append(entry)
elif any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["nursing"]):
nursing_slow.append(entry)
elif any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["admin"]):
admin_slow.append(entry)
elif any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["support"]):
support_slow.append(entry)
medical_slow.sort(key=lambda x: x["avg_days"], reverse=True)
nursing_slow.sort(key=lambda x: x["avg_days"], reverse=True)
admin_slow.sort(key=lambda x: x["avg_days"], reverse=True)
support_slow.sort(key=lambda x: x["avg_days"], reverse=True)
# G3: Per-source resolution time buckets (CCHI and MOH)
moh_complaints = all_complaints.filter(
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED],
source__name_en__icontains="moh",
)
cchi_complaints = all_complaints.filter(
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED],
source__name_en__icontains="chi",
)
moh_24h = moh_48h = moh_72h = moh_over = moh_total = 0
for c in moh_complaints:
moh_total += 1
if c.resolved_at and c.created_at:
hours = (c.resolved_at - c.created_at).total_seconds() / 3600
if hours <= 24:
moh_24h += 1
elif hours <= 48:
moh_48h += 1
elif hours <= 72:
moh_72h += 1
else:
moh_over += 1
cchi_24h = cchi_48h = cchi_72h = cchi_over = cchi_total = 0
for c in cchi_complaints:
cchi_total += 1
if c.resolved_at and c.created_at:
hours = (c.resolved_at - c.created_at).total_seconds() / 3600
if hours <= 24:
cchi_24h += 1
elif hours <= 48:
cchi_48h += 1
elif hours <= 72:
cchi_72h += 1
else:
cchi_over += 1
# G4: Count complaints lacking responses
complaints_no_response = 0
for c in all_complaints:
has_response = c.updates.filter(update_type="response").exists()
if not has_response:
complaints_no_response += 1
escalated_count = all_complaints.filter(updates__update_type="escalation").distinct().count()
closed_count = all_complaints.filter(status=ComplaintStatus.CLOSED).count()
target_percentage = float(report.target_percentage)
threshold_percentage = float(report.threshold_percentage)
performance_status = (
"met"
if overall_percentage >= target_percentage
else "below threshold"
if overall_percentage >= threshold_percentage
else "below threshold - action needed"
)
# G5: Use resolved_count as denominator for time buckets
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}%
- Threshold: {threshold_percentage:.2f}%
- Status: {performance_status}
- Total Complaints: {total_complaints}
- Total Resolved: {resolved_count}
- Resolved within 72h: {resolved_within_72h}
RESOLUTION TIME BREAKDOWN (of resolved complaints):
- Within 24h: {resolved_24h} ({(resolved_24h / resolved_count * 100) if resolved_count else 0:.2f}%)
- Within 48h: {resolved_48h} ({(resolved_48h / resolved_count * 100) if resolved_count else 0:.2f}%)
- Within 72h: {resolved_72h} ({(resolved_72h / resolved_count * 100) if resolved_count else 0:.2f}%)
- After 72h: {resolved_over_72h} ({(resolved_over_72h / resolved_count * 100) if resolved_count else 0:.2f}%)
AVERAGE RESPONSE TIME BY DEPARTMENT CATEGORY (exceeding 48 hours):
Medical Department:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['count']} complaints)" for d in medical_slow[:10]]) if medical_slow else "- All within timeframe"}
Non-Medical Department:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['count']} complaints)" for d in admin_slow[:10]]) if admin_slow else "- All within timeframe"}
Nursing Department:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['count']} complaints)" for d in nursing_slow[:5]]) if nursing_slow else "- All within timeframe"}
Support Services:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['count']} complaints)" for d in support_slow[:5]]) if support_slow else "- No complaints received"}
COMPLAINTS LACKING RESPONSES: {complaints_no_response}
ESCALATION & CLOSURE:
- Escalated Complaints: {escalated_count}
- Closed Complaints: {closed_count}
MOH RESOLUTION BREAKDOWN ({moh_total} resolved complaints):
- Within 24h: {moh_24h} ({(moh_24h / moh_total * 100) if moh_total else 0:.2f}%)
- Within 48h: {moh_48h} ({(moh_48h / moh_total * 100) if moh_total else 0:.2f}%)
- Within 72h: {moh_72h} ({(moh_72h / moh_total * 100) if moh_total else 0:.2f}%)
- After 72h: {moh_over} ({(moh_over / moh_total * 100) if moh_total else 0:.2f}%)
CCHI RESOLUTION BREAKDOWN ({cchi_total} resolved complaints):
- Within 24h: {cchi_24h} ({(cchi_24h / cchi_total * 100) if cchi_total else 0:.2f}%)
- Within 48h: {cchi_48h} ({(cchi_48h / cchi_total * 100) if cchi_total else 0:.2f}%)
- Within 72h: {cchi_72h} ({(cchi_72h / cchi_total * 100) if cchi_total else 0:.2f}%)
- After 72h: {cchi_over} ({(cchi_over / cchi_total * 100) if cchi_total 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])}
Provide analysis in JSON format:
{{
"executive_summary": "Overview of {overall_percentage:.2f}% 72h resolution rate vs {target_percentage:.0f}% target",
"comparison_to_target": "Analysis of meeting target and threshold",
"resolution_time_analysis": {{
"within_24h": {{"count": {resolved_24h}, "percentage": "{(resolved_24h / resolved_count * 100) if resolved_count else 0:.2f}%"}},
"within_48h": {{"count": {resolved_48h}, "percentage": "{(resolved_48h / resolved_count * 100) if resolved_count else 0:.2f}%"}},
"within_72h": {{"count": {resolved_72h}, "percentage": "{(resolved_72h / resolved_count * 100) if resolved_count else 0:.2f}%"}},
"after_72h": {{"count": {resolved_over_72h}, "percentage": "{(resolved_over_72h / resolved_count * 100) if resolved_count else 0:.2f}%"}}
}},
"slow_departments": {{
"medical": ["Dept Name: X Days"],
"non_medical": ["Dept Name: X Days"],
"nursing": ["Dept Name: X Days"],
"support_services": ["Dept Name: X Days"]
}},
"source_resolution_breakdown": {{
"moh": {{"total": {moh_total}, "within_24h": {moh_24h}, "within_48h": {moh_48h}, "within_72h": {moh_72h}, "after_72h": {moh_over}}},
"cchi": {{"total": {cchi_total}, "within_24h": {cchi_24h}, "within_48h": {cchi_48h}, "within_72h": {cchi_72h}, "after_72h": {cchi_over}}}
}},
"escalation_closure": {{
"escalated": {escalated_count},
"closed": {closed_count},
"complaints_no_response": {complaints_no_response}
}},
"key_findings": ["Finding 1", "Finding 2"],
"recommendations": ["Recommendation 1", "Recommendation 2"]
}}"""
system_prompt = """You are a healthcare KPI analysis expert. Analyze 72-Hour Complaint Resolution data and provide:
1. Executive summary with performance status vs target and threshold
2. Resolution time breakdown analysis
3. Slow departments analysis by category (Medical, Non-Medical, Nursing, Support)
4. Source-specific resolution insights (MOH vs CCHI breakdown)
5. Escalation and response status
6. Key findings with specific numbers
7. Actionable recommendations
Be specific and use actual numbers from the data."""
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 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_patient_experience_analysis(cls, report: KPIReport) -> dict:
"""Generate AI analysis for Patient Experience Score (MOH-1) report"""
from apps.core.ai_service import AIService
from apps.surveys.models import SurveyInstance, SurveyStatus
try:
overall_percentage = float(report.overall_result) if report.overall_result else 0
total_surveyed = report.total_denominator
satisfied_count = report.total_numerator
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,
"avg_score": float(md.details.get("avg_score", 0)) if md.details else 0,
}
)
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 django.db.models import Avg as AvgAgg, Count
surveys = SurveyInstance.objects.filter(
survey_template__hospital=report.hospital,
survey_template__survey_type__in=["stage", "general"],
status=SurveyStatus.COMPLETED,
completed_at__gte=year_start,
completed_at__lt=year_end,
)
total_completed = surveys.count()
avg_score_val = surveys.aggregate(avg=AvgAgg("total_score"))["avg"] or 0
dissatisfied = surveys.filter(total_score__lt=4).count()
neutral_count = surveys.filter(total_score__gte=3, total_score__lt=4).count()
very_satisfied = surveys.filter(total_score__gte=4.5).count()
negative_surveys = surveys.filter(is_negative=True).count()
delivery_breakdown = surveys.values("delivery_channel").annotate(count=Count("id")).order_by("-count")
delivery_lines = []
for d in delivery_breakdown:
label = dict(SurveyInstance._meta.get_field("delivery_channel").choices).get(
d["delivery_channel"], d["delivery_channel"]
)
pct = (d["count"] / total_completed * 100) if total_completed else 0
delivery_lines.append(f"- {label}: {d['count']} ({pct:.1f}%)")
lowest_month = min(monthly_breakdown, key=lambda x: x["percentage"]) if monthly_breakdown else None
highest_month = max(monthly_breakdown, key=lambda x: x["percentage"]) if monthly_breakdown else None
prev_month = report.month - 1 if report.month > 1 else 12
prev_year = report.year if report.month > 1 else report.year - 1
prev_percentage = None
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 None
except KPIReport.DoesNotExist:
pass
target_percentage = float(report.target_percentage)
threshold_percentage = float(report.threshold_percentage)
performance_status = (
"Target Met"
if overall_percentage >= target_percentage
else "Below Target"
if overall_percentage >= threshold_percentage
else "Below Threshold"
)
prev_trend = ""
if prev_percentage is not None:
diff = overall_percentage - prev_percentage
direction = "improved" if diff > 0 else "declined" if diff < 0 else "stable"
prev_trend = f" ({direction} from {prev_month}/{prev_year}: {prev_percentage:.1f}%)"
prompt = f"""Analyze this Patient Experience Score (MOH-1) KPI report.
REPORT DATA:
- Report Period: {report.year}-{report.month:02d}
- Hospital: {report.hospital.name}
- Patient Experience Score: {overall_percentage:.2f}%{prev_trend}
- Target: {target_percentage:.2f}%
- Threshold: {threshold_percentage:.2f}%
- Status: {performance_status}
- Total Patients Surveyed: {total_surveyed}
- Total Completed Responses: {total_completed}
- Average Score: {avg_score_val:.2f}
SATISFACTION BREAKDOWN:
- Very Satisfied (score >= 4.5): {very_satisfied} ({(very_satisfied / total_completed * 100) if total_completed else 0:.1f}%)
- Satisfied/Good (score >= 4): {satisfied_count} ({(satisfied_count / total_completed * 100) if total_completed else 0:.1f}%)
- Neutral (3-4): {neutral_count} ({(neutral_count / total_completed * 100) if total_completed else 0:.1f}%)
- Dissatisfied (<3): {dissatisfied} ({(dissatisfied / total_completed * 100) if total_completed else 0:.1f}%)
- Flagged as Negative: {negative_surveys}
DELIVERY CHANNEL BREAKDOWN:
{chr(10).join(delivery_lines) if delivery_lines else "- No data"}
MONTHLY TRENDS:
{chr(10).join([f"- {m['month_name']}: {m['percentage']:.2f}% ({m['satisfied']}/{m['total']}, avg score: {m['avg_score']:.2f})" for m in monthly_breakdown])}
LOWEST: {lowest_month["month_name"] if lowest_month else "N/A"} ({lowest_month["percentage"]:.2f}%)
HIGHEST: {highest_month["month_name"] if highest_month else "N/A"} ({highest_month["percentage"]:.2f}%)
Provide analysis in JSON format:
{{
"executive_summary": "Overview of {overall_percentage:.2f}% patient experience score vs {target_percentage:.0f}% target",
"comparison_to_target": "Analysis vs target ({target_percentage:.0f}%) and threshold ({threshold_percentage:.0f}%){prev_trend}",
"patient_experience_score": "{overall_percentage:.2f}%",
"satisfaction_breakdown": {{
"very_satisfied": {very_satisfied},
"satisfied_good": {satisfied_count},
"neutral": {neutral_count},
"dissatisfied": {dissatisfied},
"negative_flagged": {negative_surveys}
}},
"delivery_channel_analysis": "Analysis of survey delivery channels",
"monthly_trend": ["Month: Percentage%"],
"response_statistics": {{
"total_surveyed": {total_surveyed},
"total_completed": {total_completed},
"average_score": "{avg_score_val:.2f}"
}},
"key_findings": ["Finding 1", "Finding 2"],
"recommendations": ["Recommendation 1", "Recommendation 2"]
}}"""
system_prompt = """You are a healthcare patient experience analysis expert for MOH-1 Patient Experience Score.
Analyze the patient experience survey data and provide:
1. Executive summary of patient experience performance
2. Comparison to target (85%) and threshold (78%)
3. Previous month trend comparison
4. Satisfaction breakdown analysis
5. Delivery channel insights
6. Month-by-month trend analysis
7. Key findings with specific numbers
8. Actionable recommendations for improving patient experience
Focus on identifying drivers of satisfaction and dissatisfaction."""
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 Patient Experience report {report.id}")
return analysis
except Exception as e:
logger.exception(f"Error generating Patient Experience 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
from apps.surveys.models import SurveyInstance, SurveyStatus
try:
overall_percentage = float(report.overall_result) if report.overall_result else 0
total_responses = report.total_denominator
satisfied_responses = report.total_numerator
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,
}
)
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)
)
total_complaints_received = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=year_end,
complaint_type="complaint",
).count()
surveys = SurveyInstance.objects.filter(
survey_template__hospital=report.hospital,
survey_template__survey_type="complaint_resolution",
completed_at__gte=year_start,
completed_at__lt=year_end,
)
completed_surveys = surveys.filter(status=SurveyStatus.COMPLETED)
completed_count = completed_surveys.count()
no_response = total_complaints_received - completed_count
dissatisfied_count = 0
neutral_count = 0
for survey in completed_surveys:
score = survey.total_score
if score is not None:
if score >= 4:
pass
elif score <= 2:
dissatisfied_count += 1
else:
neutral_count += 1
else:
for resp in survey.responses.all():
if resp.question and (
"satisfaction" in resp.question.text.lower() or "satisfied" in resp.question.text.lower()
):
val = resp.numeric_value
if val is not None:
val_int = int(val)
if val_int <= 2:
dissatisfied_count += 1
elif val_int == 3:
neutral_count += 1
else:
text = (resp.text_value or resp.choice_value or "").lower()
if any(w in text for w in ["dissatisfied", "unhappy", "bad", "poor"]):
dissatisfied_count += 1
else:
neutral_count += 1
break
response_rate = (completed_count / total_complaints_received * 100) if total_complaints_received > 0 else 0
satisfaction_excl_no_response = (satisfied_responses / completed_count * 100) if completed_count > 0 else 0
moh_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=year_end,
complaint_type="complaint",
source__name_en__icontains="moh",
).count()
cchi_complaints = Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=year_end,
complaint_type="complaint",
source__name_en__icontains="chi",
).count()
target_percentage = float(report.target_percentage)
threshold_percentage = float(report.threshold_percentage)
performance_status = (
"target met"
if overall_percentage >= target_percentage
else "below target but above threshold"
if overall_percentage >= threshold_percentage
else "below threshold - action needed"
)
lowest_month = min(monthly_breakdown, key=lambda x: x["percentage"]) if monthly_breakdown else None
highest_month = max(monthly_breakdown, key=lambda x: x["percentage"]) if monthly_breakdown else None
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}%
- Threshold: {threshold_percentage:.2f}%
- Status: {performance_status}
SATISFACTION BREAKDOWN:
- Total Complaints Received: {total_complaints_received}
- Total Responses: {completed_count}
- No Response: {no_response}
- Satisfied: {satisfied_responses}
- Not Satisfied: {dissatisfied_count}
- Neutral: {neutral_count}
SATISFACTION RATE: {satisfaction_excl_no_response:.2f}% (after excluding no responses)
RESPONSE RATE: {response_rate:.2f}% ({completed_count}/{total_complaints_received})
MONTHLY SATISFACTION TRENDS:
{chr(10).join([f"- {m['month_name']}: {m['percentage']:.2f}% ({m['satisfied']}/{m['total']})" for m in monthly_breakdown])}
LOWEST PERFORMANCE: {lowest_month["month_name"] if lowest_month else "N/A"} ({lowest_month["percentage"]:.2f}%)
HIGHEST PERFORMANCE: {highest_month["month_name"] if highest_month else "N/A"} ({highest_month["percentage"]:.2f}%)
SOURCE BREAKDOWN:
- MOH complaints: {moh_complaints}
- CCHI complaints: {cchi_complaints}
Provide analysis in JSON format:
{{
"executive_summary": "Overview of {overall_percentage:.2f}% satisfaction rate vs {target_percentage:.0f}% target",
"comparison_to_target": "Analysis of meeting target ({target_percentage:.0f}%) and threshold ({threshold_percentage:.0f}%)",
"satisfaction_rate": "{satisfaction_excl_no_response:.2f}%",
"response_rate": "{response_rate:.2f}%",
"participation_analysis": "Analysis of {response_rate:.1f}% response rate and {no_response} complaints with no response",
"monthly_trend": [
"Month: Percentage%"
],
"satisfaction_breakdown": {{
"satisfied": {satisfied_responses},
"not_satisfied": {dissatisfied_count},
"neutral": {neutral_count}
}},
"response_statistics": {{
"total_complaints_received": {total_complaints_received},
"total_responses": {completed_count},
"no_response": {no_response},
"satisfied": {satisfied_responses},
"not_satisfied": {dissatisfied_count},
"neutral": {neutral_count}
}},
"key_issues": [
"Issue 1: description",
"Issue 2: description"
],
"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. Comparison to target and threshold (80% target, 70% threshold)
3. Satisfaction rate analysis (excluding non-responses)
4. Response rate / participation analysis
5. Month-by-month satisfaction rate analysis with trends
6. Key issues causing dissatisfaction
7. 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:
overall_percentage = float(report.overall_result) if report.overall_result else 0
total_complaints = report.total_denominator
responded_within_48h = report.total_numerator
# Previous month comparison
prev_month = report.month - 1 if report.month > 1 else 12
prev_year = report.year if report.month > 1 else report.year - 1
prev_percentage = None
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 None
except KPIReport.DoesNotExist:
pass
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)
)
all_complaints = (
Complaint.objects.filter(
hospital=report.hospital,
created_at__gte=year_start,
created_at__lt=year_end,
complaint_type="complaint",
)
.select_related("department", "source")
.prefetch_related("updates")
)
responded_48_72h = 0
responded_over_72h = 0
complaints_no_response = 0
dept_response_times = {}
for c in all_complaints:
first_response = (
c.updates.filter(update_type__in=["communication", "resolution"]).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 hours > 48:
if hours <= 72:
responded_48_72h += 1
else:
responded_over_72h += 1
else:
complaints_no_response += 1
total_exceeding_48h = responded_48_72h + responded_over_72h
escalated_count = all_complaints.filter(updates__update_type="escalation").distinct().count()
medical_slow = []
nursing_slow = []
admin_slow = []
support_slow = []
for dept, days_list in dept_response_times.items():
if days_list:
avg_days = sum(days_list) / len(days_list)
entry = {"name": dept, "avg_days": round(avg_days, 1), "complaint_count": len(days_list)}
name_lower = dept.lower()
if any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["medical"]):
if avg_days > 2:
medical_slow.append(entry)
elif any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["nursing"]):
if avg_days > 2:
nursing_slow.append(entry)
elif any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["admin"]):
if avg_days > 2:
admin_slow.append(entry)
elif any(k in name_lower for k in DEPARTMENT_CATEGORY_KEYWORDS["support"]):
if avg_days > 2:
support_slow.append(entry)
medical_slow.sort(key=lambda x: x["avg_days"], reverse=True)
nursing_slow.sort(key=lambda x: x["avg_days"], reverse=True)
admin_slow.sort(key=lambda x: x["avg_days"], reverse=True)
support_slow.sort(key=lambda x: x["avg_days"], reverse=True)
target_percentage = float(report.target_percentage)
threshold_percentage = float(report.threshold_percentage)
performance_status = (
"Target Met"
if overall_percentage >= target_percentage
else "Below Target"
if overall_percentage >= threshold_percentage
else "Below Threshold"
)
prev_trend = ""
if prev_percentage is not None:
diff = overall_percentage - prev_percentage
direction = "improved" if diff > 0 else "declined" if diff < 0 else "stable"
prev_trend = f" ({direction} from {prev_month}/{prev_year}: {prev_percentage:.1f}%)"
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}%{prev_trend}
- Target: {target_percentage:.2f}%
- Threshold: {threshold_percentage:.2f}%
- Status: {performance_status}
- 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}%)
- Escalated due to delays: {escalated_count}
- No response: {complaints_no_response}
DEPARTMENTS WITH MORE THAN 48 HOURS OF RESPONSE:
MEDICAL DEPARTMENT:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['complaint_count']} complaints)" for d in medical_slow[:10]]) if medical_slow else "- None exceeding timeframe"}
NON-MEDICAL DEPARTMENT:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['complaint_count']} complaints)" for d in admin_slow[:10]]) if admin_slow else "- All within timeframe"}
NURSING DEPARTMENT:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['complaint_count']} complaints)" for d in nursing_slow[:5]]) if nursing_slow else "- None exceeding timeframe"}
SUPPORT SERVICES:
{chr(10).join([f"- {d['name']}: {d['avg_days']} Days ({d['complaint_count']} complaints)" 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 ({target_percentage:.0f}%) and threshold ({threshold_percentage:.0f}%){prev_trend}",
"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}%"}}
}},
"escalated_due_to_delays": {escalated_count},
"complaints_no_response": {complaints_no_response},
"slow_departments": {{
"medical": ["Dept Name: X Days"],
"non_medical": ["Dept Name: X Days"],
"nursing": ["Dept Name: X Days"],
"support_services": ["Dept Name: X Days"]
}},
"key_findings": ["Finding 1", "Finding 2"],
"recommendations": ["Recommendation 1", "Recommendation 2"]
}}"""
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
3. Previous month trend comparison
4. Breakdown of complaints exceeding 48 hours
5. Escalated complaints due to department delays
6. Analysis of slow departments by category (Medical, Nursing, Non-Medical, Support)
7. Specific department names with their average response times
8. 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:
overall_percentage = float(report.overall_result) if report.overall_result else 0
total_complaints = report.total_denominator
activated_within_2h = report.total_numerator
month_start = datetime(report.year, report.month, 1)
month_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=month_start,
created_at__lt=month_end,
complaint_type="complaint",
).select_related("assigned_to")
activated_complaints = []
delayed_complaints = []
staff_activation = {}
staff_delays = {}
for c in complaints:
if c.assigned_at and c.created_at:
hours = (c.assigned_at - c.created_at).total_seconds() / 3600
staff_name = c.assigned_to.get_full_name() if c.assigned_to 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
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)
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 = float(report.threshold_percentage) if report.threshold_percentage else 90.0
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
from apps.dashboard.models import ComplaintRequest
try:
overall_percentage = float(report.overall_result) if report.overall_result else 0
total_complaints = report.total_denominator
unactivated_filled = report.total_numerator
month_start = datetime(report.year, report.month, 1)
month_end = (
datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1)
)
all_requests = ComplaintRequest.objects.filter(
complaint__hospital=report.hospital,
complaint__created_at__gte=month_start,
complaint__created_at__lt=month_end,
complaint__complaint_type="complaint",
).select_related("staff", "complaint")
on_hold_count = all_requests.filter(on_hold=True).count()
filled_count = all_requests.filter(filled=True).count()
not_filled_count = all_requests.filter(not_filled=True).count()
barcode_count = all_requests.filter(from_barcode=True).count()
filling_times = dict(ComplaintRequest.FILLING_TIME_CHOICES)
time_breakdown = {}
for t in ComplaintRequest.FILLING_TIME_CHOICES:
time_breakdown[t[0]] = all_requests.filter(filling_time_category=t[0]).count()
staff_stats = {}
for req in all_requests:
staff_name = req.staff.get_full_name() if req.staff 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 req.filled:
staff_stats[staff_name]["filled"] += 1
if req.not_filled:
staff_stats[staff_name]["not_filled"] += 1
staff_table = sorted(
[
{"name": s, "total": v["total"], "filled": v["filled"], "not_filled": v["not_filled"]}
for s, v in staff_stats.items()
],
key=lambda x: x["total"],
reverse=True,
)
target_percentage = float(report.target_percentage)
threshold_percentage = float(report.threshold_percentage) if report.threshold_percentage else 5.0
performance_status = (
"Within Target"
if overall_percentage <= target_percentage
else "Above Threshold - Action Needed"
if overall_percentage <= threshold_percentage
else "Critical - Exceeds Acceptable Limit"
)
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 Filled Rate: {overall_percentage:.2f}%
- Target: {target_percentage:.2f}%
- Threshold: {threshold_percentage:.2f}%
- Status: {performance_status}
- Total Complaints: {total_complaints}
- Unactivated Filled: {unactivated_filled}
COMPLAINT REQUEST BREAKDOWN:
- On Hold: {on_hold_count}
- Not Filled: {not_filled_count}
- Filled: {filled_count}
- From Barcode: {barcode_count}
TIME TO FILL ANALYSIS:
- Same Time: {time_breakdown.get("same_time", 0)}
- Within 6 Hours: {time_breakdown.get("within_6h", 0)}
- 6 Hours to 24 Hours: {time_breakdown.get("6_to_24h", 0)}
- After 1 Day: {time_breakdown.get("after_1_day", 0)}
- Time Not Mentioned: {time_breakdown.get("not_mentioned", 0)}
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]]) if staff_table else "- No data"}
Provide analysis in JSON format:
{{
"executive_summary": "Overview of {overall_percentage:.2f}% unactivated filled rate vs {threshold_percentage:.0f}% threshold",
"threshold_analysis": "Rate {"exceeds" if overall_percentage > threshold_percentage else "is within"} acceptable threshold of {threshold_percentage:.0f}%",
"complaint_statistics": {{
"on_hold": {on_hold_count},
"not_filled": {not_filled_count},
"filled": {filled_count},
"from_barcode": {barcode_count}
}},
"time_to_fill": {{
"same_time": {time_breakdown.get("same_time", 0)},
"within_6h": {time_breakdown.get("within_6h", 0)},
"6h_to_24h": {time_breakdown.get("6_to_24h", 0)},
"after_1d": {time_breakdown.get("after_1_day", 0)},
"not_mentioned": {time_breakdown.get("not_mentioned", 0)}
}},
"staff_breakdown": [
"Staff Name: Total X, Filled Y, Not Filled Z"
],
"key_findings": ["Finding 1", "Finding 2"],
"recommendations": [
"Monitoring the workflow and activation times among employees",
"Improve system notifications for prompt 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 (target: 0%, threshold: 5%)
2. Threshold analysis
3. Complaint request statistics (on hold, filled, not filled, barcode)
4. Time to fill analysis
5. Staff breakdown and performance
6. Key findings with specific numbers
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"}