2366 lines
96 KiB
Python
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"}
|