484 lines
16 KiB
Python
484 lines
16 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, 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
|
|
|
|
|
|
@login_required
|
|
def analytics_dashboard(request):
|
|
"""
|
|
Analytics dashboard with KPIs and charts.
|
|
|
|
Features:
|
|
- KPI cards with current values
|
|
- Trend charts
|
|
- Department rankings
|
|
- Physician rankings
|
|
"""
|
|
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
|
|
|
|
# Calculate key metrics
|
|
complaints_queryset = Complaint.objects.all()
|
|
actions_queryset = PXAction.objects.all()
|
|
surveys_queryset = SurveyInstance.objects.filter(status='completed')
|
|
|
|
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)
|
|
|
|
# KPI calculations
|
|
kpis = {
|
|
'total_complaints': complaints_queryset.count(),
|
|
'open_complaints': complaints_queryset.filter(status='open').count(),
|
|
'overdue_complaints': complaints_queryset.filter(is_overdue=True).count(),
|
|
'total_actions': actions_queryset.count(),
|
|
'open_actions': actions_queryset.filter(status='open').count(),
|
|
'overdue_actions': actions_queryset.filter(is_overdue=True).count(),
|
|
'avg_survey_score': surveys_queryset.aggregate(avg=Avg('total_score'))['avg'] or 0,
|
|
'negative_surveys': surveys_queryset.filter(is_negative=True).count(),
|
|
}
|
|
|
|
# Department rankings (top 5 by survey score)
|
|
department_rankings = Department.objects.filter(
|
|
status='active'
|
|
).annotate(
|
|
avg_score=Avg('journey_stages__survey_instance__total_score'),
|
|
survey_count=Count('journey_stages__survey_instance')
|
|
).filter(
|
|
survey_count__gt=0
|
|
).order_by('-avg_score')[:5]
|
|
|
|
# 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)
|
|
|
|
context = {
|
|
'kpis': kpis,
|
|
'department_rankings': department_rankings,
|
|
'hospitals': hospitals,
|
|
'selected_hospital': hospital,
|
|
}
|
|
|
|
return render(request, 'analytics/dashboard.html', context)
|
|
|
|
|
|
@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)
|
|
|
|
|
|
@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)
|
|
|
|
|
|
@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')
|
|
.order_by('due_at')[:20]
|
|
.values(
|
|
'id',
|
|
'title',
|
|
'severity',
|
|
'due_at',
|
|
hospital_name=F('hospital__name'),
|
|
department_name=F('department__name'),
|
|
patient_name=Concat('patient__first_name', Value(' '), 'patient__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
|
|
})
|
|
|
|
|
|
@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_name=Concat('patient__first_name', Value(' '), 'patient__last_name'),
|
|
hospital_name=F('hospital__name'),
|
|
department_name=F('department__name')
|
|
)
|
|
.values_list(
|
|
'id',
|
|
'title',
|
|
'patient_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)
|