Removed ~300 lines of redundant hospital filtering code from views. Templates no longer use hospital dropdowns, so views don't need to: - Query Hospital.objects.filter() - Apply RBAC filtering to hospitals queryset - Pass hospitals to context The middleware (TenantMiddleware) already handles hospital filtering via request.tenant_hospital for all users. Files cleaned: - apps/surveys/ui_views.py - apps/callcenter/ui_views.py - apps/complaints/ui_views.py - apps/analytics/ui_views.py - apps/physicians/ui_views.py - apps/projects/ui_views.py - apps/feedback/views.py - apps/dashboard/views.py - apps/journeys/ui_views.py - apps/appreciation/ui_views.py
692 lines
25 KiB
Python
692 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
|
|
|
|
kpis = {
|
|
"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),
|
|
"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),
|
|
"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),
|
|
"total_feedback": total_feedback,
|
|
"compliments": compliments,
|
|
"suggestions": suggestions,
|
|
"avg_rating": round(avg_rating, 2),
|
|
}
|
|
|
|
context = {
|
|
"kpis": kpis,
|
|
"selected_hospital": hospital,
|
|
"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_sources": serialize_queryset_values(action_sources),
|
|
"action_categories": serialize_queryset_values(action_categories),
|
|
"survey_types": serialize_queryset_values(survey_types),
|
|
"survey_score_trend": serialize_queryset_values(survey_score_trend),
|
|
"sentiment_breakdown": serialize_queryset_values(sentiment_breakdown),
|
|
"feedback_categories": serialize_queryset_values(feedback_categories),
|
|
"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,
|
|
"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)
|