716 lines
25 KiB
Python
716 lines
25 KiB
Python
"""
|
|
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) |