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