""" Analytics Console UI views """ from datetime import datetime from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Avg, Count, F, Q, Value from django.db.models.functions import Concat from django.http import JsonResponse from django.shortcuts import render from apps.complaints.models import Complaint from apps.organizations.models import Department, Hospital from apps.px_action_center.models import PXAction from apps.surveys.models import SurveyInstance from apps.physicians.models import PhysicianMonthlyRating from .models import KPI, KPIValue from .services import UnifiedAnalyticsService, ExportService from apps.core.decorators import block_source_user import json def serialize_queryset_values(queryset): """Properly serialize QuerySet values to JSON string.""" if queryset is None: return '[]' data = list(queryset) result = [] for item in data: row = {} for key, value in item.items(): # Convert UUID to string if hasattr(value, 'hex'): # UUID object row[key] = str(value) # Convert Python None to JavaScript null elif value is None: row[key] = None else: row[key] = value result.append(row) return json.dumps(result, default=str) @block_source_user @login_required def analytics_dashboard(request): """ Analytics dashboard with KPIs and charts. Comprehensive dashboard showing: - KPI cards with current values for Complaints, Actions, Surveys, Feedback - Trend charts - Department rankings - Source distribution - Status breakdown """ from apps.feedback.models import Feedback from django.utils import timezone from datetime import timedelta from django.db.models.functions import TruncDate, TruncMonth user = request.user # Get hospital filter hospital_filter = request.GET.get('hospital') if hospital_filter: hospital = Hospital.objects.filter(id=hospital_filter).first() elif user.hospital: hospital = user.hospital else: hospital = None # Base querysets complaints_queryset = Complaint.objects.all() actions_queryset = PXAction.objects.all() surveys_queryset = SurveyInstance.objects.filter(status='completed') feedback_queryset = Feedback.objects.all() if hospital: complaints_queryset = complaints_queryset.filter(hospital=hospital) actions_queryset = actions_queryset.filter(hospital=hospital) surveys_queryset = surveys_queryset.filter(survey_template__hospital=hospital) feedback_queryset = feedback_queryset.filter(hospital=hospital) # ============ COMPLAINTS KPIs ============ total_complaints = complaints_queryset.count() open_complaints = complaints_queryset.filter(status='open').count() in_progress_complaints = complaints_queryset.filter(status='in_progress').count() resolved_complaints = complaints_queryset.filter(status='resolved').count() closed_complaints = complaints_queryset.filter(status='closed').count() overdue_complaints = complaints_queryset.filter(is_overdue=True).count() # Complaint sources complaint_sources = complaints_queryset.values('source').annotate(count=Count('id')).order_by('-count')[:6] # Complaint domains (Level 1) top_domains = complaints_queryset.filter(domain__isnull=False).values('domain__name_en').annotate(count=Count('id')).order_by('-count')[:5] # Complaint categories (Level 2) top_categories = complaints_queryset.filter(category__isnull=False).values('category__name_en').annotate(count=Count('id')).order_by('-count')[:5] # Complaint severity severity_breakdown = complaints_queryset.values('severity').annotate(count=Count('id')).order_by('-count') # Status breakdown status_breakdown = complaints_queryset.values('status').annotate(count=Count('id')).order_by('-count') # ============ ACTIONS KPIs ============ total_actions = actions_queryset.count() open_actions = actions_queryset.filter(status='open').count() in_progress_actions = actions_queryset.filter(status='in_progress').count() approved_actions = actions_queryset.filter(status='approved').count() closed_actions = actions_queryset.filter(status='closed').count() overdue_actions = actions_queryset.filter(is_overdue=True).count() # Action sources action_sources = actions_queryset.values('source_type').annotate(count=Count('id')).order_by('-count')[:6] # Action categories action_categories = actions_queryset.exclude(category='').values('category').annotate(count=Count('id')).order_by('-count')[:5] # ============ SURVEYS KPIs ============ total_surveys = surveys_queryset.count() avg_survey_score = surveys_queryset.aggregate(avg=Avg('total_score'))['avg'] or 0 negative_surveys = surveys_queryset.filter(is_negative=True).count() # Survey completion rate all_surveys = SurveyInstance.objects.all() if hospital: all_surveys = all_surveys.filter(survey_template__hospital=hospital) total_sent = all_surveys.count() completed_surveys = all_surveys.filter(status='completed').count() completion_rate = (completed_surveys / total_sent * 100) if total_sent > 0 else 0 # Survey types survey_types = all_surveys.values('survey_template__survey_type').annotate(count=Count('id')).order_by('-count')[:5] # ============ FEEDBACK KPIs ============ total_feedback = feedback_queryset.count() compliments = feedback_queryset.filter(feedback_type='compliment').count() suggestions = feedback_queryset.filter(feedback_type='suggestion').count() # Sentiment analysis sentiment_breakdown = feedback_queryset.values('sentiment').annotate(count=Count('id')).order_by('-count') # Feedback categories feedback_categories = feedback_queryset.values('category').annotate(count=Count('id')).order_by('-count')[:5] # Average rating avg_rating = feedback_queryset.filter(rating__isnull=False).aggregate(avg=Avg('rating'))['avg'] or 0 # ============ TRENDS (Last 30 days) ============ thirty_days_ago = timezone.now() - timedelta(days=30) # Complaint trends complaint_trend = complaints_queryset.filter( created_at__gte=thirty_days_ago ).annotate( day=TruncDate('created_at') ).values('day').annotate(count=Count('id')).order_by('day') # Survey score trend survey_score_trend = surveys_queryset.filter( completed_at__gte=thirty_days_ago ).annotate( day=TruncDate('completed_at') ).values('day').annotate(avg_score=Avg('total_score')).order_by('day') # ============ DEPARTMENT RANKINGS ============ department_rankings = Department.objects.filter( status='active' ).annotate( avg_score=Avg( 'journey_instances__surveys__total_score', filter=Q(journey_instances__surveys__status='completed') ), survey_count=Count( 'journey_instances__surveys', filter=Q(journey_instances__surveys__status='completed') ), complaint_count=Count('complaints'), action_count=Count('px_actions') ).filter( survey_count__gt=0 ).order_by('-avg_score')[:7] # ============ TIME-BASED CALCULATIONS ============ # Average resolution time (complaints) resolved_with_time = complaints_queryset.filter( status__in=['resolved', 'closed'], resolved_at__isnull=False, created_at__isnull=False ) if resolved_with_time.exists(): avg_resolution_hours = resolved_with_time.annotate( resolution_time=F('resolved_at') - F('created_at') ).aggregate(avg=Avg('resolution_time'))['avg'] if avg_resolution_hours: avg_resolution_hours = avg_resolution_hours.total_seconds() / 3600 else: avg_resolution_hours = 0 else: avg_resolution_hours = 0 # Average action completion time closed_actions_with_time = actions_queryset.filter( status='closed', closed_at__isnull=False, created_at__isnull=False ) if closed_actions_with_time.exists(): avg_action_days = closed_actions_with_time.annotate( completion_time=F('closed_at') - F('created_at') ).aggregate(avg=Avg('completion_time'))['avg'] if avg_action_days: avg_action_days = avg_action_days.days else: avg_action_days = 0 else: avg_action_days = 0 # ============ SLA COMPLIANCE ============ total_with_sla = complaints_queryset.filter(due_at__isnull=False).count() resolved_within_sla = complaints_queryset.filter( status__in=['resolved', 'closed'], resolved_at__lte=F('due_at') ).count() sla_compliance = (resolved_within_sla / total_with_sla * 100) if total_with_sla > 0 else 0 # ============ NPS CALCULATION ============ # NPS = % Promoters (9-10) - % Detractors (0-6) nps_surveys = surveys_queryset.filter( survey_template__survey_type='nps', total_score__isnull=False ) if nps_surveys.exists(): promoters = nps_surveys.filter(total_score__gte=9).count() detractors = nps_surveys.filter(total_score__lte=6).count() total_nps = nps_surveys.count() nps_score = ((promoters - detractors) / total_nps * 100) if total_nps > 0 else 0 else: nps_score = 0 # Get hospitals for filter hospitals = Hospital.objects.filter(status='active') if not user.is_px_admin() and user.hospital: hospitals = hospitals.filter(id=user.hospital.id) # Build comprehensive KPI data kpis = { # Complaints 'total_complaints': total_complaints, 'open_complaints': open_complaints, 'in_progress_complaints': in_progress_complaints, 'resolved_complaints': resolved_complaints, 'closed_complaints': closed_complaints, 'overdue_complaints': overdue_complaints, 'avg_resolution_hours': round(avg_resolution_hours, 1), 'sla_compliance': round(sla_compliance, 1), # Actions 'total_actions': total_actions, 'open_actions': open_actions, 'in_progress_actions': in_progress_actions, 'approved_actions': approved_actions, 'closed_actions': closed_actions, 'overdue_actions': overdue_actions, 'avg_action_days': round(avg_action_days, 1), # Surveys 'total_surveys': total_surveys, 'avg_survey_score': round(avg_survey_score, 2), 'nps_score': round(nps_score, 1), 'negative_surveys': negative_surveys, 'completion_rate': round(completion_rate, 1), # Feedback 'total_feedback': total_feedback, 'compliments': compliments, 'suggestions': suggestions, 'avg_rating': round(avg_rating, 2), } context = { 'kpis': kpis, 'hospitals': hospitals, 'selected_hospital': hospital, # Complaint analytics - serialize properly for JSON 'complaint_sources': serialize_queryset_values(complaint_sources), 'top_domains': serialize_queryset_values(top_domains), 'top_categories': serialize_queryset_values(top_categories), 'severity_breakdown': serialize_queryset_values(severity_breakdown), 'status_breakdown': serialize_queryset_values(status_breakdown), 'complaint_trend': serialize_queryset_values(complaint_trend), # Action analytics 'action_sources': serialize_queryset_values(action_sources), 'action_categories': serialize_queryset_values(action_categories), # Survey analytics 'survey_types': serialize_queryset_values(survey_types), 'survey_score_trend': serialize_queryset_values(survey_score_trend), # Feedback analytics 'sentiment_breakdown': serialize_queryset_values(sentiment_breakdown), 'feedback_categories': serialize_queryset_values(feedback_categories), # Department rankings 'department_rankings': department_rankings, } return render(request, 'analytics/dashboard.html', context) @block_source_user @login_required def kpi_list(request): """KPI definitions list view""" queryset = KPI.objects.all() # Apply filters category_filter = request.GET.get('category') if category_filter: queryset = queryset.filter(category=category_filter) is_active = request.GET.get('is_active') if is_active == 'true': queryset = queryset.filter(is_active=True) elif is_active == 'false': queryset = queryset.filter(is_active=False) # Ordering queryset = queryset.order_by('category', 'name') # Pagination page_size = int(request.GET.get('page_size', 25)) paginator = Paginator(queryset, page_size) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'kpis': page_obj.object_list, 'filters': request.GET, } return render(request, 'analytics/kpi_list.html', context) @block_source_user @login_required def command_center(request): """ PX Command Center - Unified Dashboard Comprehensive dashboard showing all PX360 metrics: - Complaints, Surveys, Actions KPIs - Interactive charts with ApexCharts - Department and Physician rankings - Export to Excel/PDF """ user = request.user # Get filter parameters filters = { 'date_range': request.GET.get('date_range', '30d'), 'hospital': request.GET.get('hospital', ''), 'department': request.GET.get('department', ''), 'kpi_category': request.GET.get('kpi_category', ''), 'custom_start': request.GET.get('custom_start', ''), 'custom_end': request.GET.get('custom_end', ''), } # Get hospitals for filter hospitals = Hospital.objects.filter(status='active') if not user.is_px_admin() and user.hospital: hospitals = hospitals.filter(id=user.hospital.id) # Get departments for filter departments = Department.objects.filter(status='active') if filters.get('hospital'): departments = departments.filter(hospital_id=filters['hospital']) elif not user.is_px_admin() and user.hospital: departments = departments.filter(hospital=user.hospital) # Get initial KPIs custom_start = None custom_end = None if filters['custom_start'] and filters['custom_end']: custom_start = datetime.strptime(filters['custom_start'], '%Y-%m-%d') custom_end = datetime.strptime(filters['custom_end'], '%Y-%m-%d') kpis = UnifiedAnalyticsService.get_all_kpis( user=user, date_range=filters['date_range'], hospital_id=filters['hospital'] if filters['hospital'] else None, department_id=filters['department'] if filters['department'] else None, custom_start=custom_start, custom_end=custom_end ) context = { 'filters': filters, 'hospitals': hospitals, 'departments': departments, 'kpis': kpis, } return render(request, 'analytics/command_center.html', context) @block_source_user @login_required def command_center_api(request): """ API endpoint for Command Center data Returns JSON data for KPIs, charts, and tables based on filters. Used by JavaScript to dynamically update dashboard. """ if request.method != 'GET': return JsonResponse({'error': 'Only GET requests allowed'}, status=405) user = request.user # Get filter parameters date_range = request.GET.get('date_range', '30d') hospital_id = request.GET.get('hospital') department_id = request.GET.get('department') kpi_category = request.GET.get('kpi_category') custom_start_str = request.GET.get('custom_start') custom_end_str = request.GET.get('custom_end') # Parse custom dates custom_start = None custom_end = None if custom_start_str and custom_end_str: try: custom_start = datetime.strptime(custom_start_str, '%Y-%m-%d') custom_end = datetime.strptime(custom_end_str, '%Y-%m-%d') except ValueError: pass # Handle hospital_id (can be integer or UUID string) hospital_id = hospital_id if hospital_id else None # Handle department_id (UUID string) department_id = department_id if department_id else None # Get KPIs kpis = UnifiedAnalyticsService.get_all_kpis( user=user, date_range=date_range, hospital_id=hospital_id, department_id=department_id, kpi_category=kpi_category, custom_start=custom_start, custom_end=custom_end ) # Ensure numeric KPIs are proper Python types for JSON serialization numeric_kpis = [ 'total_complaints', 'open_complaints', 'overdue_complaints', 'high_severity_complaints', 'resolved_complaints', 'total_actions', 'open_actions', 'overdue_actions', 'escalated_actions', 'resolved_actions', 'total_surveys', 'negative_surveys', 'avg_survey_score', 'negative_social_mentions', 'low_call_ratings', 'total_sentiment_analyses' ] for key in numeric_kpis: if key in kpis: value = kpis[key] if value is None: kpis[key] = 0.0 if key == 'avg_survey_score' else 0 elif isinstance(value, (int, float)): # Already a number - ensure floats for specific fields if key == 'avg_survey_score': kpis[key] = float(value) else: # Try to convert to number try: kpis[key] = float(value) except (ValueError, TypeError): kpis[key] = 0.0 if key == 'avg_survey_score' else 0 # Handle nested trend data if 'complaints_trend' in kpis and isinstance(kpis['complaints_trend'], dict): trend = kpis['complaints_trend'] trend['current'] = int(trend.get('current', 0)) trend['previous'] = int(trend.get('previous', 0)) trend['percentage_change'] = float(trend.get('percentage_change', 0)) # Get chart data chart_types = [ 'complaints_trend', 'complaints_by_category', 'survey_satisfaction_trend', 'survey_distribution', 'department_performance', 'physician_leaderboard' ] charts = {} for chart_type in chart_types: charts[chart_type] = UnifiedAnalyticsService.get_chart_data( user=user, chart_type=chart_type, date_range=date_range, hospital_id=hospital_id, department_id=department_id, custom_start=custom_start, custom_end=custom_end ) # Get table data tables = {} # Overdue complaints table complaints_qs = Complaint.objects.filter(is_overdue=True) if hospital_id: complaints_qs = complaints_qs.filter(hospital_id=hospital_id) if department_id: complaints_qs = complaints_qs.filter(department_id=department_id) # Apply role-based filtering if not user.is_px_admin() and user.hospital: complaints_qs = complaints_qs.filter(hospital=user.hospital) if user.is_department_manager() and user.department: complaints_qs = complaints_qs.filter(department=user.department) tables['overdue_complaints'] = list( complaints_qs.select_related('hospital', 'department', 'patient', 'source') .order_by('due_at')[:20] .values( 'id', 'title', 'severity', 'due_at', 'complaint_source_type', hospital_name=F('hospital__name'), department_name=F('department__name'), patient_full_name=Concat('patient__first_name', Value(' '), 'patient__last_name'), source_name=F('source__name_en'), assigned_to_full_name=Concat('assigned_to__first_name', Value(' '), 'assigned_to__last_name') ) ) # Physician leaderboard table physician_data = charts.get('physician_leaderboard', {}).get('metadata', []) tables['physician_leaderboard'] = [ { 'physician_id': p['physician_id'], 'name': p['name'], 'specialization': p['specialization'], 'department': p['department'], 'rating': float(p['rating']) if p['rating'] is not None else 0.0, 'surveys': int(p['surveys']) if p['surveys'] is not None else 0, 'positive': int(p['positive']) if p['positive'] is not None else 0, 'neutral': int(p['neutral']) if p['neutral'] is not None else 0, 'negative': int(p['negative']) if p['negative'] is not None else 0 } for p in physician_data ] return JsonResponse({ 'kpis': kpis, 'charts': charts, 'tables': tables }) @block_source_user @login_required def export_command_center(request, export_format): """ Export Command Center data to Excel or PDF Args: export_format: 'excel' or 'pdf' Returns: HttpResponse with file download """ if export_format not in ['excel', 'pdf']: return JsonResponse({'error': 'Invalid export format'}, status=400) user = request.user # Get filter parameters date_range = request.GET.get('date_range', '30d') hospital_id = request.GET.get('hospital') department_id = request.GET.get('department') kpi_category = request.GET.get('kpi_category') custom_start_str = request.GET.get('custom_start') custom_end_str = request.GET.get('custom_end') # Parse custom dates custom_start = None custom_end = None if custom_start_str and custom_end_str: try: custom_start = datetime.strptime(custom_start_str, '%Y-%m-%d') custom_end = datetime.strptime(custom_end_str, '%Y-%m-%d') except ValueError: pass # Handle hospital_id and department_id (can be integer or UUID string) hospital_id = hospital_id if hospital_id else None department_id = department_id if department_id else None # Get all data kpis = UnifiedAnalyticsService.get_all_kpis( user=user, date_range=date_range, hospital_id=hospital_id, department_id=department_id, kpi_category=kpi_category, custom_start=custom_start, custom_end=custom_end ) chart_types = [ 'complaints_trend', 'complaints_by_category', 'survey_satisfaction_trend', 'survey_distribution', 'department_performance', 'physician_leaderboard' ] charts = {} for chart_type in chart_types: charts[chart_type] = UnifiedAnalyticsService.get_chart_data( user=user, chart_type=chart_type, date_range=date_range, hospital_id=hospital_id, department_id=department_id, custom_start=custom_start, custom_end=custom_end ) # Get table data tables = {} # Overdue complaints complaints_qs = Complaint.objects.filter(is_overdue=True) if hospital_id: complaints_qs = complaints_qs.filter(hospital_id=hospital_id) if department_id: complaints_qs = complaints_qs.filter(department_id=department_id) if not user.is_px_admin() and user.hospital: complaints_qs = complaints_qs.filter(hospital=user.hospital) if user.is_department_manager() and user.department: complaints_qs = complaints_qs.filter(department=user.department) tables['overdue_complaints'] = { 'headers': ['ID', 'Title', 'Patient', 'Severity', 'Hospital', 'Department', 'Due Date'], 'rows': list( complaints_qs.select_related('hospital', 'department', 'patient') .order_by('due_at')[:100] .annotate( patient_full_name=Concat('patient__first_name', Value(' '), 'patient__last_name'), hospital_name=F('hospital__name'), department_name=F('department__name') ) .values_list( 'id', 'title', 'patient_full_name', 'severity', 'hospital_name', 'department_name', 'due_at' ) ) } # Physician leaderboard physician_data = charts.get('physician_leaderboard', {}).get('metadata', []) tables['physician_leaderboard'] = { 'headers': ['Name', 'Specialization', 'Department', 'Rating', 'Surveys', 'Positive', 'Neutral', 'Negative'], 'rows': [ [ p['name'], p['specialization'], p['department'], str(p['rating']), str(p['surveys']), str(p['positive']), str(p['neutral']), str(p['negative']) ] for p in physician_data ] } # Prepare export data export_data = ExportService.prepare_dashboard_data( user=user, kpis=kpis, charts=charts, tables=tables ) # Export based on format if export_format == 'excel': return ExportService.export_to_excel(export_data) elif export_format == 'pdf': return ExportService.export_to_pdf(export_data) return JsonResponse({'error': 'Export failed'}, status=500)